mdeck 3.4.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.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/assets/extensions/palettes/cobalt/extension.toml +30 -0
- package/assets/extensions/palettes/greyscale/extension.toml +57 -0
- package/assets/extensions/themes/glass/extension.toml +59 -0
- package/assets/extensions/themes/glass/styles.css +754 -0
- package/assets/extensions/themes/plain/extension.toml +58 -0
- package/assets/extensions/themes/plain/styles.css +471 -0
- package/assets/extensions/themes/work/extension.toml +57 -0
- package/assets/extensions/themes/work/styles.css +512 -0
- package/docs/reference/authoring.md +25 -8
- package/docs/reference/palettes.md +4 -3
- package/docs/reference/themes.md +5 -3
- package/docs/site/content/appearance.md +24 -10
- package/docs/site/content/audience.md +14 -0
- package/docs/site/content/theme-repository.md +7 -7
- package/docs/site/images/themes/academic-dark.webp +0 -0
- package/docs/site/images/themes/academic-light.webp +0 -0
- package/docs/site/images/themes/academic.webp +0 -0
- package/docs/site/images/themes/glass-dark.webp +0 -0
- package/docs/site/images/themes/glass-light.webp +0 -0
- package/docs/site/images/themes/glass.webp +0 -0
- package/docs/site/images/themes/minimal-dark.webp +0 -0
- package/docs/site/images/themes/minimal-light.webp +0 -0
- package/docs/site/images/themes/minimal.webp +0 -0
- package/docs/site/images/themes/neue-dark.webp +0 -0
- package/docs/site/images/themes/neue-light.webp +0 -0
- package/docs/site/images/themes/neue.webp +0 -0
- package/docs/site/images/themes/plain-dark.webp +0 -0
- package/docs/site/images/themes/plain-light.webp +0 -0
- package/docs/site/images/themes/plain.webp +0 -0
- package/docs/site/images/themes/work-dark.webp +0 -0
- package/docs/site/images/themes/work-light.webp +0 -0
- package/docs/site/images/themes/work.webp +0 -0
- package/examples/showcase/slides.md +2 -2
- package/package.json +4 -1
- package/skills/write-slides/SKILL.md +2 -2
- package/src/build/slidesPlugin.js +16 -15
- package/src/components/Poll.jsx +17 -5
- package/src/components/inlineText.js +38 -15
- package/src/components/poll.css +3 -0
- package/src/core/editDeck.js +4 -5
- package/src/core/pollList.js +66 -0
- package/src/core/tags.js +195 -0
- package/src/extensions/discover.js +2 -2
- package/src/extensions/moved.js +4 -4
- package/src/layouts/layoutApi.jsx +20 -10
- package/src/live/roomTag.js +11 -19
- package/src/runtime/main.jsx +2 -1
- package/src/runtime/markedSetup.js +3 -0
- package/assets/extensions/palettes/neon/extension.toml +0 -30
- package/assets/extensions/themes/aurora/extension.toml +0 -40
- package/assets/extensions/themes/aurora/styles.css +0 -670
- package/docs/site/images/themes/aurora-dark.webp +0 -0
- package/docs/site/images/themes/aurora-light.webp +0 -0
- package/docs/site/images/themes/aurora.webp +0 -0
package/src/core/tags.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Marked } from 'marked'
|
|
2
|
+
import markedKatex from 'marked-katex-extension'
|
|
3
|
+
|
|
4
|
+
// The HTML tags in a deck's Markdown, found the way the slide finds them:
|
|
5
|
+
// marked decides what is HTML (not code, not a tag a blank line cut in two),
|
|
6
|
+
// and the tags in that HTML are read the way a browser reads them, so a `>`
|
|
7
|
+
// in a quoted value (question="Is $x > 0$?") does not end its tag. Every
|
|
8
|
+
// part of mdeck that looks for tags in a deck's text goes through here.
|
|
9
|
+
|
|
10
|
+
const lexer = new Marked().use(markedKatex({ throwOnError: false, output: 'html' }))
|
|
11
|
+
|
|
12
|
+
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' }
|
|
13
|
+
const decode = value => value.replace(/&(?:#(\d+)|#x([0-9a-f]+)|([a-z]+));/gi, (entity, dec, hex, name) =>
|
|
14
|
+
dec ? String.fromCodePoint(Number(dec)) : hex ? String.fromCodePoint(parseInt(hex, 16)) : ENTITIES[name.toLowerCase()] ?? entity)
|
|
15
|
+
|
|
16
|
+
const SPACE = /[\t\n\f\r ]/
|
|
17
|
+
// Elements whose content is text, not tags.
|
|
18
|
+
const RAW_TEXT = new Set(['script', 'style', 'textarea', 'title'])
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The tags in a piece of HTML, in order: { name, attrs, spans, raw, index,
|
|
22
|
+
* end, closing, selfClosing }, `spans` giving where each value is in `raw`. Attribute values are decoded and a repeated
|
|
23
|
+
* attribute keeps its first value, as in the browser. Comments are skipped,
|
|
24
|
+
* and a `<` that starts no tag is text.
|
|
25
|
+
*/
|
|
26
|
+
export function scanTags(html) {
|
|
27
|
+
const tags = []
|
|
28
|
+
let i = 0
|
|
29
|
+
while ((i = html.indexOf('<', i)) >= 0) {
|
|
30
|
+
if (html.startsWith('<!--', i)) {
|
|
31
|
+
const close = html.indexOf('-->', i + 4)
|
|
32
|
+
i = close < 0 ? html.length : close + 3
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
const tag = readTag(html, i)
|
|
36
|
+
if (!tag) { i++; continue }
|
|
37
|
+
tags.push(tag)
|
|
38
|
+
i = tag.end
|
|
39
|
+
if (!tag.closing && !tag.selfClosing && RAW_TEXT.has(tag.name)) {
|
|
40
|
+
const close = html.toLowerCase().indexOf(`</${tag.name}`, i)
|
|
41
|
+
i = close < 0 ? html.length : close
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return tags
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readTag(html, start) {
|
|
48
|
+
let i = start + 1
|
|
49
|
+
const closing = html[i] === '/'
|
|
50
|
+
if (closing) i++
|
|
51
|
+
const name = /^[a-z][^\t\n\f\r />]*/i.exec(html.slice(i))?.[0]
|
|
52
|
+
if (!name) return null
|
|
53
|
+
i += name.length
|
|
54
|
+
const attrs = {}
|
|
55
|
+
// Where each attribute's value is in `raw`, as [from, to].
|
|
56
|
+
const spans = {}
|
|
57
|
+
let selfClosing = false
|
|
58
|
+
while (i < html.length) {
|
|
59
|
+
while (SPACE.test(html[i] ?? '')) i++
|
|
60
|
+
if (html[i] === '>') return { name: name.toLowerCase(), attrs, spans, raw: html.slice(start, i + 1), index: start, end: i + 1, closing, selfClosing }
|
|
61
|
+
if (html[i] === '/') { selfClosing = html[i + 1] === '>'; i++; continue }
|
|
62
|
+
if (i >= html.length) break
|
|
63
|
+
selfClosing = false
|
|
64
|
+
let attr = html[i++]
|
|
65
|
+
while (i < html.length && !SPACE.test(html[i]) && !'/>='.includes(html[i])) attr += html[i++]
|
|
66
|
+
while (SPACE.test(html[i] ?? '')) i++
|
|
67
|
+
let value = ''
|
|
68
|
+
let span = [i - start, i - start]
|
|
69
|
+
if (html[i] === '=') {
|
|
70
|
+
i++
|
|
71
|
+
while (SPACE.test(html[i] ?? '')) i++
|
|
72
|
+
const quote = html[i]
|
|
73
|
+
if (quote === '"' || quote === "'") {
|
|
74
|
+
const close = html.indexOf(quote, i + 1)
|
|
75
|
+
if (close < 0) return null
|
|
76
|
+
value = html.slice(i + 1, close)
|
|
77
|
+
span = [i + 1 - start, close - start]
|
|
78
|
+
i = close + 1
|
|
79
|
+
} else {
|
|
80
|
+
const from = i
|
|
81
|
+
while (i < html.length && !SPACE.test(html[i]) && html[i] !== '>') i++
|
|
82
|
+
value = html.slice(from, i)
|
|
83
|
+
span = [from - start, i - start]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
attr = attr.toLowerCase()
|
|
87
|
+
if (!(attr in attrs)) { attrs[attr] = decode(value); spans[attr] = span }
|
|
88
|
+
}
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The children of a token, whatever kind it is.
|
|
93
|
+
const childrenOf = token => [
|
|
94
|
+
...token.tokens ?? [],
|
|
95
|
+
...token.items ?? [],
|
|
96
|
+
...(token.header ?? []).flatMap(cell => cell.tokens ?? []),
|
|
97
|
+
...(token.rows ?? []).flat().flatMap(cell => cell.tokens ?? []),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The HTML in a piece of Markdown, as marked reads it: [{ text, start }],
|
|
102
|
+
* where `start` is the HTML's offset in the source, or null where it cannot
|
|
103
|
+
* be told (such as a tag spread over several lines of a quotation).
|
|
104
|
+
*/
|
|
105
|
+
export function htmlIn(markdown = '') {
|
|
106
|
+
const source = String(markdown ?? '').replace(/\r\n?/g, '\n')
|
|
107
|
+
const found = []
|
|
108
|
+
// A child's raw text is found inside its parent's, after its older sibling.
|
|
109
|
+
const walk = (tokens, parent, parentStart) => {
|
|
110
|
+
let cursor = 0
|
|
111
|
+
for (const token of tokens) {
|
|
112
|
+
const at = parentStart == null || token.raw == null ? -1 : parent.indexOf(token.raw, cursor)
|
|
113
|
+
const start = at < 0 ? null : parentStart + at
|
|
114
|
+
if (at >= 0) cursor = at + token.raw.length
|
|
115
|
+
if (token.type === 'html') found.push({ text: token.raw ?? token.text, start })
|
|
116
|
+
const children = childrenOf(token)
|
|
117
|
+
if (children.length) walk(children, token.raw ?? '', start)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
walk(lexer.lexer(source), source, 0)
|
|
121
|
+
return found
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The tags in a piece of Markdown, opening and closing, in order, each with
|
|
126
|
+
* `start` and `end` in the source (null where it cannot be told). Tags in
|
|
127
|
+
* code are not tags.
|
|
128
|
+
*/
|
|
129
|
+
export function tagsIn(markdown = '') {
|
|
130
|
+
return htmlIn(markdown).flatMap(({ text, start }) => scanTags(text).map(tag => ({
|
|
131
|
+
...tag,
|
|
132
|
+
start: start == null ? null : start + tag.index,
|
|
133
|
+
end: start == null ? null : start + tag.end,
|
|
134
|
+
})))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The closing tag of `tags[at]` in a list from tagsIn or scanTags, past any
|
|
139
|
+
* of the same name inside it; null when it has none.
|
|
140
|
+
*/
|
|
141
|
+
export function closingTagOf(tags, at) {
|
|
142
|
+
const open = tags[at]
|
|
143
|
+
if (open.closing || open.selfClosing) return null
|
|
144
|
+
let depth = 0
|
|
145
|
+
for (const tag of tags.slice(at + 1)) {
|
|
146
|
+
if (tag.name !== open.name || tag.selfClosing) continue
|
|
147
|
+
if (!tag.closing) depth++
|
|
148
|
+
else if (depth) depth--
|
|
149
|
+
else return tag
|
|
150
|
+
}
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Self-closing tags of the named elements given a closing tag. A browser
|
|
156
|
+
* takes `<poll />` for `<poll>`, so what follows would end up inside it.
|
|
157
|
+
*/
|
|
158
|
+
export function closeTags(html, names) {
|
|
159
|
+
let out = ''
|
|
160
|
+
let last = 0
|
|
161
|
+
for (const tag of scanTags(html)) {
|
|
162
|
+
if (!tag.selfClosing || !names.has(tag.name)) continue
|
|
163
|
+
out += html.slice(last, tag.index) + tag.raw.replace(/\s*\/>$/, '>') + `</${tag.name}>`
|
|
164
|
+
last = tag.end
|
|
165
|
+
}
|
|
166
|
+
return out + html.slice(last)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The opening tags (and self-closing ones) in a piece of Markdown. */
|
|
170
|
+
export const openTagsIn = markdown => tagsIn(markdown).filter(tag => !tag.closing)
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Calls `fn(tag)` for every tag in a piece of Markdown and puts what it
|
|
174
|
+
* returns in its place (the tag stays when it returns undefined). Line ends
|
|
175
|
+
* come back as \n.
|
|
176
|
+
*/
|
|
177
|
+
export function replaceTagsIn(markdown, fn) {
|
|
178
|
+
const source = String(markdown ?? '').replace(/\r\n?/g, '\n')
|
|
179
|
+
let out = ''
|
|
180
|
+
let last = 0
|
|
181
|
+
for (const tag of tagsIn(source)) {
|
|
182
|
+
if (tag.start == null || tag.start < last) continue
|
|
183
|
+
const replacement = fn(tag)
|
|
184
|
+
if (replacement == null) continue
|
|
185
|
+
out += source.slice(last, tag.start) + replacement
|
|
186
|
+
last = tag.end
|
|
187
|
+
}
|
|
188
|
+
return out + source.slice(last)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** A tag's text with one attribute's value replaced (as written, not encoded). */
|
|
192
|
+
export function withAttribute(tag, name, value) {
|
|
193
|
+
const [from, to] = tag.spans[name]
|
|
194
|
+
return tag.raw.slice(0, from) + value + tag.raw.slice(to)
|
|
195
|
+
}
|
|
@@ -21,8 +21,8 @@ export function userExtensionsDir() {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// The built-in themes and palettes this user removed (`mdeck themes remove
|
|
24
|
-
//
|
|
25
|
-
// "theme:
|
|
24
|
+
// glass`): they stay in mdeck's folder but are not offered. Kept as
|
|
25
|
+
// "theme:glass" and "palette:nordic".
|
|
26
26
|
const removedFile = () => resolve(mdeckHome(), 'removed.json')
|
|
27
27
|
export function removedBuiltIns() {
|
|
28
28
|
let saved = {}
|
package/src/extensions/moved.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// Themes and palettes that came with mdeck
|
|
2
|
-
//
|
|
1
|
+
// Themes and palettes that once came with mdeck and now live in the theme
|
|
2
|
+
// repository. A deck that names one is told how to install it instead
|
|
3
3
|
// of being told the name is unknown.
|
|
4
4
|
export const MOVED = {
|
|
5
|
-
themes: ['duet', 'editorial', 'fhnw', 'terminal', 'sketch'],
|
|
6
|
-
palettes: ['
|
|
5
|
+
themes: ['duet', 'editorial', 'fhnw', 'terminal', 'sketch', 'aurora'],
|
|
6
|
+
palettes: ['terra', 'brand', 'phosphor', 'pastel', 'forest', 'neon'],
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
// Palettes that were removed because another one looks almost the same, by
|
|
@@ -3,6 +3,8 @@ import { useEffect, useRef } from 'preact/hooks'
|
|
|
3
3
|
import { render } from 'preact'
|
|
4
4
|
import { marked, Parser } from 'marked'
|
|
5
5
|
import { registry } from '../runtime/registry'
|
|
6
|
+
import { scanTags, closeTags } from '../core/tags.js'
|
|
7
|
+
import { expandPollLists } from '../core/pollList.js'
|
|
6
8
|
import { InkLayer } from '../runtime/ink/InkLayer.jsx'
|
|
7
9
|
import themedSvgs from 'virtual:deck-svgs'
|
|
8
10
|
import { renderCitations, citationsHtml as referencesOf, bibliographyOf } from '../runtime/citations.js'
|
|
@@ -39,7 +41,9 @@ function preprocessFootnotes(markdown) {
|
|
|
39
41
|
export { t } from '../core/labels.js'
|
|
40
42
|
|
|
41
43
|
export function extractContent(markdown, { headingLevels = [], paragraph = false } = {}) {
|
|
42
|
-
|
|
44
|
+
// marked's preprocess hook (src/runtime/markedSetup.js) does not run for
|
|
45
|
+
// marked.lexer, so the lists of polls become their options here.
|
|
46
|
+
const processed = expandPollLists(markdown)
|
|
43
47
|
const footnotesHtml = ''
|
|
44
48
|
const tokens = marked.lexer(processed)
|
|
45
49
|
const headings = {}
|
|
@@ -74,17 +78,19 @@ function themedDataSvg(src) {
|
|
|
74
78
|
return /var\(--/.test(markup) && /^\s*<svg\b/.test(markup) ? markup : null
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
const attribute = (tag, name) => tag.match(new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, 'i'))?.slice(1).find(v => v != null)
|
|
78
|
-
|
|
79
81
|
// Pictures in slide text keep the drawing's own size, like an <img>.
|
|
80
82
|
export function inlineThemedPictures(html) {
|
|
81
83
|
if (!html || !html.includes('<img')) return html
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
let out = ''
|
|
85
|
+
let last = 0
|
|
86
|
+
for (const tag of scanTags(html)) {
|
|
87
|
+
const markup = tag.name === 'img' && !tag.closing && themedSvg(tag.attrs.src)
|
|
88
|
+
if (!markup) continue
|
|
89
|
+
const alt = (tag.attrs.alt ?? '').replace(/&/g, '&').replace(/"/g, '"')
|
|
90
|
+
out += html.slice(last, tag.index) + `<span class="themed-picture themed-picture--inline" role="img" aria-label="${alt}">${markup}</span>`
|
|
91
|
+
last = tag.end
|
|
92
|
+
}
|
|
93
|
+
return out + html.slice(last)
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
// A picture from the slide's settings; `fit: cover` fills its box like a
|
|
@@ -100,6 +106,10 @@ export function Picture({ src, alt, fit, position, class: className, ...attribut
|
|
|
100
106
|
|
|
101
107
|
// ─── Hydration for inline components ──────────────────────────────────────
|
|
102
108
|
|
|
109
|
+
// Components may be written `<poll … />`, which a browser takes for an open
|
|
110
|
+
// tag, putting what follows inside the component, where it is lost.
|
|
111
|
+
const COMPONENT_TAGS = new Set(Object.keys(registry))
|
|
112
|
+
|
|
103
113
|
export function HtmlContent({ html, class: className, ...attributes }) {
|
|
104
114
|
const ref = useRef()
|
|
105
115
|
|
|
@@ -119,7 +129,7 @@ export function HtmlContent({ html, class: className, ...attributes }) {
|
|
|
119
129
|
return () => mounted.forEach(wrapper => render(null, wrapper))
|
|
120
130
|
}, [html])
|
|
121
131
|
|
|
122
|
-
return <div class={className} {...attributes} ref={ref} dangerouslySetInnerHTML={{ __html: inlineThemedPictures(html) }} />
|
|
132
|
+
return <div class={className} {...attributes} ref={ref} dangerouslySetInnerHTML={{ __html: closeTags(inlineThemedPictures(html), COMPONENT_TAGS) }} />
|
|
123
133
|
}
|
|
124
134
|
|
|
125
135
|
// ─── Header / footer / footnotes rails ────────────────────────────────────
|
package/src/live/roomTag.js
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
// Finds the component tag a phone should see: the one whose `room` attribute
|
|
2
2
|
// matches, with its content if it has any.
|
|
3
|
-
|
|
3
|
+
import { tagsIn, closingTagOf } from '../core/tags.js'
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
const blank = text => text.replace(/[^\n]/g, ' ')
|
|
8
|
-
export function maskCode(source = '') {
|
|
9
|
-
return source.replace(/^(```+|~~~+)[^\n]*\n[\s\S]*?^\1[ \t]*$/gm, blank).replace(/`[^`\n]+`/g, blank)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Inside a tag: anything but its end, with quoted values whole, so a `>` in
|
|
13
|
-
// an attribute (question="Is $x > 0$?") does not end the tag.
|
|
14
|
-
const INSIDE = `(?:[^>"']|"[^"]*"|'[^']*')`
|
|
15
|
-
|
|
16
|
-
// Every activity in a source, in order: [{ tag, room }].
|
|
5
|
+
// Every activity in a source, in order: [{ tag, room }]. Tags in code are
|
|
6
|
+
// not activities (src/core/tags.js).
|
|
17
7
|
export function roomsIn(source = '') {
|
|
18
|
-
return
|
|
8
|
+
return tagsIn(source).filter(tag => !tag.closing && tag.attrs.room).map(tag => ({ tag: tag.name, room: tag.attrs.room }))
|
|
19
9
|
}
|
|
20
10
|
|
|
21
11
|
// The rooms on one slide, from its body and its named regions (the body is
|
|
@@ -34,9 +24,11 @@ export function slideTitleFor(deck, room) {
|
|
|
34
24
|
}
|
|
35
25
|
|
|
36
26
|
export function findRoomTag(source, room) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
const text = String(source ?? '').replace(/\r\n?/g, '\n')
|
|
28
|
+
const tags = tagsIn(text)
|
|
29
|
+
const at = tags.findIndex(tag => !tag.closing && tag.attrs.room === room)
|
|
30
|
+
if (at < 0) return null
|
|
31
|
+
const open = tags[at]
|
|
32
|
+
const close = open.start == null ? null : closingTagOf(tags, at)
|
|
33
|
+
return close?.end == null ? open.raw : text.slice(open.start, close.end)
|
|
42
34
|
}
|
package/src/runtime/main.jsx
CHANGED
|
@@ -26,6 +26,7 @@ import { strokePath } from '../core/ink.js'
|
|
|
26
26
|
import { inkFileName } from 'virtual:deck-ink'
|
|
27
27
|
import savedResults from 'virtual:deck-results'
|
|
28
28
|
import { roomsIn, roomsOnSlide, findRoomTag, slideTitleFor } from '../live/roomTag.js'
|
|
29
|
+
import { expandPollLists } from '../core/pollList.js'
|
|
29
30
|
import './reader.css'
|
|
30
31
|
import { SlideRenderer, manifests } from '../layouts/renderSlide'
|
|
31
32
|
import { setCalloutLabels } from './markedSetup'
|
|
@@ -81,7 +82,7 @@ function activityOn(deck, slide) {
|
|
|
81
82
|
const room = roomsOnSlide(slide)[0]
|
|
82
83
|
if (!room) return { room: null, activity: null }
|
|
83
84
|
const tag = findRoomTag(slide.content ?? '', room) ?? Object.values(slide.regions ?? {}).map(region => findRoomTag(region.content ?? '', room)).find(Boolean)
|
|
84
|
-
const element = tag && new DOMParser().parseFromString(tag, 'text/html').body.firstElementChild
|
|
85
|
+
const element = tag && new DOMParser().parseFromString(expandPollLists(tag), 'text/html').body.firstElementChild
|
|
85
86
|
const component = element && registry[element.localName]
|
|
86
87
|
if (typeof component?.phone !== 'function') return { room, activity: null }
|
|
87
88
|
const props = Object.fromEntries([...element.attributes].map(attribute => [attribute.name, attribute.value]))
|
|
@@ -5,6 +5,7 @@ import '../components/callout.css'
|
|
|
5
5
|
import '../components/columns.css'
|
|
6
6
|
import { scanDirectives, sourceLines, fenceState } from '../core/source.js'
|
|
7
7
|
import { CALLOUT_LABELS, labelSetFor } from '../core/labels.js'
|
|
8
|
+
import { expandPollLists } from '../core/pollList.js'
|
|
8
9
|
|
|
9
10
|
function readDirective(src, name) {
|
|
10
11
|
if (!/^:::\s*[\w-]+/.test(src)) return null
|
|
@@ -36,6 +37,8 @@ function escapeHtml(text) {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
marked.use(markedKatex({ throwOnError: false, output: 'html' }))
|
|
40
|
+
// A poll's options written as a list inside the tag (src/core/pollList.js).
|
|
41
|
+
marked.use({ hooks: { preprocess: expandPollLists } })
|
|
39
42
|
|
|
40
43
|
// marked.use() prepends via unshift, so last-registered = highest priority.
|
|
41
44
|
// Register callout first (lowest priority) so columns and steps match before it.
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
schema = 1
|
|
2
|
-
kind = "palette"
|
|
3
|
-
id = "neon"
|
|
4
|
-
title = "Neon"
|
|
5
|
-
description = "Violet and magenta; loud, for workshops."
|
|
6
|
-
version = "1.0.0"
|
|
7
|
-
author = "Tilman Schieber"
|
|
8
|
-
license = "MIT"
|
|
9
|
-
|
|
10
|
-
[light]
|
|
11
|
-
"--bg" = "#ffffff"
|
|
12
|
-
"--surface" = "#f4f0fb"
|
|
13
|
-
"--ink" = "#14072b"
|
|
14
|
-
"--ink-soft" = "#3a2d55"
|
|
15
|
-
"--muted" = "#6a6084"
|
|
16
|
-
"--rule" = "#e4ddf3"
|
|
17
|
-
"--accent" = "#9b00d9"
|
|
18
|
-
"--accent-2" = "#d6106f"
|
|
19
|
-
"--on-accent" = "#ffffff"
|
|
20
|
-
|
|
21
|
-
[dark]
|
|
22
|
-
"--bg" = "#0d0d0d"
|
|
23
|
-
"--surface" = "#1a1a1a"
|
|
24
|
-
"--ink" = "#f0f0f0"
|
|
25
|
-
"--ink-soft" = "#cccccc"
|
|
26
|
-
"--muted" = "#8a8a8a"
|
|
27
|
-
"--rule" = "#2a2a2a"
|
|
28
|
-
"--accent" = "#cc17ff"
|
|
29
|
-
"--accent-2" = "#ff1791"
|
|
30
|
-
"--on-accent" = "#0d0d0d"
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
schema = 1
|
|
2
|
-
kind = "theme"
|
|
3
|
-
id = "aurora"
|
|
4
|
-
title = "Aurora"
|
|
5
|
-
description = "Northern lights: curtains of light in the accent colours, night-sky chapter and statement slides, frosted glass."
|
|
6
|
-
version = "1.0.0"
|
|
7
|
-
author = "Tilman Schieber"
|
|
8
|
-
license = "MIT"
|
|
9
|
-
# The palette this theme uses unless the deck sets one; colours come only from palettes.
|
|
10
|
-
palette = "neon"
|
|
11
|
-
fonts = [
|
|
12
|
-
"https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;600;700;800&display=swap",
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
[files]
|
|
16
|
-
styles = "styles.css"
|
|
17
|
-
|
|
18
|
-
# Type, spacing and fonts used by styles.css; colours come from the palette.
|
|
19
|
-
# Deck settings (params) override these.
|
|
20
|
-
[tokens]
|
|
21
|
-
|
|
22
|
-
# Type scale (px) — projection-appropriate at 1920×1080
|
|
23
|
-
"--fs-display" = "168px"
|
|
24
|
-
"--fs-title" = "112px"
|
|
25
|
-
"--fs-focus" = "100px"
|
|
26
|
-
"--fs-h" = "76px"
|
|
27
|
-
"--fs-sub" = "44px"
|
|
28
|
-
"--fs-body" = "34px"
|
|
29
|
-
"--fs-small" = "26px"
|
|
30
|
-
"--fs-eyebrow" = "20px"
|
|
31
|
-
|
|
32
|
-
# Spacing
|
|
33
|
-
"--pad-x" = "120px"
|
|
34
|
-
"--pad-y" = "72px"
|
|
35
|
-
"--gap-title" = "48px"
|
|
36
|
-
"--gap-item" = "24px"
|
|
37
|
-
|
|
38
|
-
# Type families
|
|
39
|
-
"--font-display" = "\"Plus Jakarta Sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif"
|
|
40
|
-
"--font-body" = "\"Plus Jakarta Sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif"
|