@abreen/tada 1.14.2 → 1.15.1
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/README.md +4 -2
- package/build/utils/markdown.ts +220 -0
- package/init/content/labs/02/index.md +22 -7
- package/init/content/markdown.md +11 -0
- package/package.json +1 -1
- package/src/_base.scss +13 -0
- package/src/question/index.ts +60 -0
- package/src/question/style.scss +130 -10
- package/src/slides/index.ts +20 -10
- package/src/slides/style.scss +5 -5
- package/templates/_theme.scss +2 -2
- package/templates/_top.html +4 -4
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ A static site generator. The successor to Presto.
|
|
|
27
27
|
* Renders step-by-step source highlighting, output, and memory diagrams
|
|
28
28
|
- Slides mode: separate content by `---` and present sections as full-screen slides
|
|
29
29
|
- PDF files are copied into `dist/`
|
|
30
|
-
|
|
30
|
+
* Text of each PDF page is extracted using `mutool` (if present) and indexed
|
|
31
31
|
- External link handling (special visual treatment for external links)
|
|
32
32
|
- Internal link validation at build time (broken links fail the build)
|
|
33
33
|
- Internal links automatically prefixed with base path, if specified
|
|
@@ -37,7 +37,8 @@ A static site generator. The successor to Presto.
|
|
|
37
37
|
* `<<< details ... <<<` renders a collapsible box
|
|
38
38
|
* `::: section ... :::` renders a special section with a fancy background
|
|
39
39
|
* `!!! note ... !!!` and `!!! warning ... !!!` render alert boxes
|
|
40
|
-
* `??? question ... ???` renders a Q
|
|
40
|
+
* `??? question ... ???` renders a Q&A section; answer is hidden until click
|
|
41
|
+
- To display clickable multiple choice options, use a [task list][task-list]
|
|
41
42
|
* `+++ ... +++ ... +++` renders a two-column layout
|
|
42
43
|
* Special heading subtitles with `## Heading # A subtitle here`
|
|
43
44
|
* `{{{ _partial.md }}}` syntax for including partials
|
|
@@ -387,3 +388,4 @@ which results in:
|
|
|
387
388
|
[jep467]: https://openjdk.org/jeps/467
|
|
388
389
|
[katex]: https://katex.org/
|
|
389
390
|
[homebrew]: https://brew.sh/
|
|
391
|
+
[task-list]: https://github.github.com/gfm/#task-list-items-extension-
|
package/build/utils/markdown.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { RenderDependencyCollector, SiteVariables } from '../types';
|
|
|
24
24
|
const DETAILS_PATTERN = /^details\s+(.*)$/;
|
|
25
25
|
const ALERT_PATTERN = /^(note|warning)(?:\s+"(.+)"|\s+(.+))?$/;
|
|
26
26
|
const QUESTION_PATTERN = /^question\s+(.+)$/;
|
|
27
|
+
const MULTIPLE_CHOICE_MARKER_LENGTH = 4;
|
|
27
28
|
|
|
28
29
|
interface CreateMarkdownOptions {
|
|
29
30
|
filePath?: string;
|
|
@@ -36,6 +37,16 @@ interface CreateMarkdownOptions {
|
|
|
36
37
|
dependencyCollector?: RenderDependencyCollector;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
interface MultipleChoiceOption {
|
|
41
|
+
correct: boolean;
|
|
42
|
+
inline: Token;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface MultipleChoiceMeta {
|
|
46
|
+
question: string;
|
|
47
|
+
options: MultipleChoiceOption[];
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
function capitalize(str: string): string {
|
|
40
51
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
41
52
|
}
|
|
@@ -123,6 +134,132 @@ export function createMarkdown(
|
|
|
123
134
|
return token;
|
|
124
135
|
}
|
|
125
136
|
|
|
137
|
+
function cloneToken(token: Token): Token {
|
|
138
|
+
const cloned = new Token(token.type, token.tag, token.nesting);
|
|
139
|
+
cloned.attrs = token.attrs?.map(([name, value]) => [name, value]) ?? null;
|
|
140
|
+
cloned.block = token.block;
|
|
141
|
+
cloned.children = token.children?.map(cloneToken) ?? null;
|
|
142
|
+
cloned.content = token.content;
|
|
143
|
+
cloned.hidden = token.hidden;
|
|
144
|
+
cloned.info = token.info;
|
|
145
|
+
cloned.level = token.level;
|
|
146
|
+
cloned.map = token.map ? [token.map[0], token.map[1]] : null;
|
|
147
|
+
cloned.markup = token.markup;
|
|
148
|
+
cloned.meta = token.meta;
|
|
149
|
+
return cloned;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseMultipleChoiceOption(
|
|
153
|
+
inline: Token,
|
|
154
|
+
): MultipleChoiceOption | null {
|
|
155
|
+
const firstChild = inline.children?.[0];
|
|
156
|
+
if (firstChild?.type !== 'text') {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let correct: boolean;
|
|
161
|
+
if (firstChild.content.startsWith('[ ] ')) {
|
|
162
|
+
correct = false;
|
|
163
|
+
} else if (
|
|
164
|
+
firstChild.content.startsWith('[x] ') ||
|
|
165
|
+
firstChild.content.startsWith('[X] ')
|
|
166
|
+
) {
|
|
167
|
+
correct = true;
|
|
168
|
+
} else {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const optionInline = cloneToken(inline);
|
|
173
|
+
optionInline.content = inline.content.slice(MULTIPLE_CHOICE_MARKER_LENGTH);
|
|
174
|
+
if (optionInline.children?.[0]?.type === 'text') {
|
|
175
|
+
optionInline.children[0].content = optionInline.children[0].content.slice(
|
|
176
|
+
MULTIPLE_CHOICE_MARKER_LENGTH,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (slides) {
|
|
180
|
+
sanitizeTokenForSlides(optionInline);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { correct, inline: optionInline };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function findQuestionCloseIndex(tokens: Token[], openIndex: number): number {
|
|
187
|
+
const level = tokens[openIndex].level;
|
|
188
|
+
for (let i = openIndex + 1; i < tokens.length; i++) {
|
|
189
|
+
const token = tokens[i];
|
|
190
|
+
if (token.type === 'container_question_close' && token.level === level) {
|
|
191
|
+
return i;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return -1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function multipleChoiceError(message: string): Error {
|
|
198
|
+
return new Error(
|
|
199
|
+
(filePath ? `${filePath}: ` : '') + `multiple choice question ${message}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseMultipleChoiceQuestion(
|
|
204
|
+
tokens: Token[],
|
|
205
|
+
openIndex: number,
|
|
206
|
+
closeIndex: number,
|
|
207
|
+
): MultipleChoiceMeta | null {
|
|
208
|
+
const questionMatch = tokens[openIndex].info.trim().match(QUESTION_PATTERN);
|
|
209
|
+
if (!questionMatch) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const bodyStart = openIndex + 1;
|
|
214
|
+
const bodyEnd = closeIndex - 1;
|
|
215
|
+
if (
|
|
216
|
+
bodyStart > bodyEnd ||
|
|
217
|
+
tokens[bodyStart].type !== 'bullet_list_open' ||
|
|
218
|
+
tokens[bodyEnd].type !== 'bullet_list_close'
|
|
219
|
+
) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const options: MultipleChoiceOption[] = [];
|
|
224
|
+
for (let i = bodyStart + 1; i < bodyEnd; ) {
|
|
225
|
+
const listItemOpen = tokens[i];
|
|
226
|
+
const paragraphOpen = tokens[i + 1];
|
|
227
|
+
const inline = tokens[i + 2];
|
|
228
|
+
const paragraphClose = tokens[i + 3];
|
|
229
|
+
const listItemClose = tokens[i + 4];
|
|
230
|
+
if (
|
|
231
|
+
listItemOpen?.type !== 'list_item_open' ||
|
|
232
|
+
paragraphOpen?.type !== 'paragraph_open' ||
|
|
233
|
+
inline?.type !== 'inline' ||
|
|
234
|
+
paragraphClose?.type !== 'paragraph_close' ||
|
|
235
|
+
listItemClose?.type !== 'list_item_close'
|
|
236
|
+
) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const option = parseMultipleChoiceOption(inline);
|
|
241
|
+
if (!option) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
options.push(option);
|
|
246
|
+
i += 5;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (options.length === 0) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const correctCount = options.filter(option => option.correct).length;
|
|
254
|
+
if (correctCount !== 1) {
|
|
255
|
+
throw multipleChoiceError(
|
|
256
|
+
`must include exactly one correct option marked with [x], but found ${correctCount}`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { question: questionMatch[1], options };
|
|
261
|
+
}
|
|
262
|
+
|
|
126
263
|
function isInlineOnlyTokenStream(tokens: Token[]): boolean {
|
|
127
264
|
return (
|
|
128
265
|
tokens.length === 1 &&
|
|
@@ -434,6 +571,89 @@ export function createMarkdown(
|
|
|
434
571
|
},
|
|
435
572
|
});
|
|
436
573
|
|
|
574
|
+
markdown.core.ruler.before(
|
|
575
|
+
'slides_transform',
|
|
576
|
+
'multiple_choice_questions',
|
|
577
|
+
state => {
|
|
578
|
+
const transformedTokens: Token[] = [];
|
|
579
|
+
for (let i = 0; i < state.tokens.length; i++) {
|
|
580
|
+
const token = state.tokens[i];
|
|
581
|
+
if (token.type !== 'container_question_open') {
|
|
582
|
+
transformedTokens.push(token);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const closeIndex = findQuestionCloseIndex(state.tokens, i);
|
|
587
|
+
if (closeIndex === -1) {
|
|
588
|
+
transformedTokens.push(token);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const multipleChoice = parseMultipleChoiceQuestion(
|
|
593
|
+
state.tokens,
|
|
594
|
+
i,
|
|
595
|
+
closeIndex,
|
|
596
|
+
);
|
|
597
|
+
if (!multipleChoice) {
|
|
598
|
+
transformedTokens.push(...state.tokens.slice(i, closeIndex + 1));
|
|
599
|
+
i = closeIndex;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const multipleChoiceToken = new Token(
|
|
604
|
+
'question_multiple_choice',
|
|
605
|
+
'',
|
|
606
|
+
0,
|
|
607
|
+
);
|
|
608
|
+
multipleChoiceToken.block = true;
|
|
609
|
+
multipleChoiceToken.level = token.level;
|
|
610
|
+
multipleChoiceToken.map = token.map;
|
|
611
|
+
multipleChoiceToken.meta = multipleChoice;
|
|
612
|
+
transformedTokens.push(multipleChoiceToken);
|
|
613
|
+
i = closeIndex;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
state.tokens = transformedTokens;
|
|
617
|
+
},
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
markdown.renderer.rules.question_multiple_choice = (
|
|
621
|
+
tokens,
|
|
622
|
+
idx,
|
|
623
|
+
renderOptions,
|
|
624
|
+
env,
|
|
625
|
+
) => {
|
|
626
|
+
const meta = tokens[idx].meta as MultipleChoiceMeta;
|
|
627
|
+
const question = markdown.renderInline(meta.question);
|
|
628
|
+
const optionsHtml = meta.options
|
|
629
|
+
.map(option => {
|
|
630
|
+
const correctAttribute = option.correct ? ' data-correct=""' : '';
|
|
631
|
+
const optionHtml = markdown.renderer.render(
|
|
632
|
+
[option.inline],
|
|
633
|
+
renderOptions,
|
|
634
|
+
env,
|
|
635
|
+
);
|
|
636
|
+
return (
|
|
637
|
+
'<div class="question-multiple-choice-option"' +
|
|
638
|
+
correctAttribute +
|
|
639
|
+
'>' +
|
|
640
|
+
optionHtml +
|
|
641
|
+
'</div>'
|
|
642
|
+
);
|
|
643
|
+
})
|
|
644
|
+
.join('');
|
|
645
|
+
|
|
646
|
+
return (
|
|
647
|
+
'<div class="question question-multiple-choice">' +
|
|
648
|
+
'<p class="question-q"><span class="question-label">Q.</span><span>' +
|
|
649
|
+
question +
|
|
650
|
+
'</span></p>' +
|
|
651
|
+
'<div class="question-multiple-choice-options">' +
|
|
652
|
+
optionsHtml +
|
|
653
|
+
'</div></div>\n'
|
|
654
|
+
);
|
|
655
|
+
};
|
|
656
|
+
|
|
437
657
|
/*
|
|
438
658
|
* Customize markdown-it-footnote renderer.
|
|
439
659
|
*
|
|
@@ -28,10 +28,8 @@ Selection sort's strength is its simplicity.
|
|
|
28
28
|
- `index_smallest()` always scans from `start` to the end
|
|
29
29
|
- It requires no additional memory (other than local variables)
|
|
30
30
|
|
|
31
|
-
However
|
|
32
|
-
|
|
33
|
-
- It has no `break` statements & can't return early
|
|
34
|
-
- No difference between best, average and worst cases
|
|
31
|
+
However, it has no `break` statements and therefore can't return early if
|
|
32
|
+
the array is already sorted.
|
|
35
33
|
|
|
36
34
|
---
|
|
37
35
|
|
|
@@ -48,10 +46,10 @@ However
|
|
|
48
46
|
The innermost statement compares `nums[i]` and `nums[curr]`.
|
|
49
47
|
|
|
50
48
|
- when `i` is 0, the comparison is executed $n - 1$ times
|
|
51
|
-
- when `i` is 1, the
|
|
49
|
+
- when `i` is 1, the comparison is executed $n - 2$ times
|
|
52
50
|
- $\dots$
|
|
53
|
-
- when `i` is $n - 3$, the
|
|
54
|
-
- when `i` is $n - 2$, the
|
|
51
|
+
- when `i` is $n - 3$, the comparison is executed two times
|
|
52
|
+
- when `i` is $n - 2$, the comparison is executed one time
|
|
55
53
|
|
|
56
54
|
Therefore the total number of comparisons is
|
|
57
55
|
|
|
@@ -59,3 +57,20 @@ $$
|
|
|
59
57
|
(n-1) + (n-2) + \dots + 1 = \sum_{i=1}^{n-1}i
|
|
60
58
|
$$
|
|
61
59
|
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Review
|
|
63
|
+
|
|
64
|
+
??? question The outer loop of selection sort runs
|
|
65
|
+
- [ ] $n$ times
|
|
66
|
+
- [x] $n - 1$ times
|
|
67
|
+
- [ ] $n^2$ times
|
|
68
|
+
- [ ] $n - 2$ times
|
|
69
|
+
???
|
|
70
|
+
|
|
71
|
+
??? question What is selection sort's best case?
|
|
72
|
+
- [ ] An already sorted array
|
|
73
|
+
- [ ] A reverse-sorted array
|
|
74
|
+
- [ ] An array filled with the same element
|
|
75
|
+
- [x] Its best, worst, and average cases are all the same
|
|
76
|
+
???
|
package/init/content/markdown.md
CHANGED
|
@@ -293,6 +293,16 @@ solved directly without any further recursive calls. For recursive methods that
|
|
|
293
293
|
process strings, the base case is the empty string.
|
|
294
294
|
???
|
|
295
295
|
|
|
296
|
+
If the body is a [task list][task-list] with exactly one `[x]` option, it
|
|
297
|
+
becomes a multiple choice question with clickable options:
|
|
298
|
+
|
|
299
|
+
??? question Which is correct for obtaining the number of characters in a `String`?
|
|
300
|
+
- [ ] `word.size()`
|
|
301
|
+
- [x] `word.length()`
|
|
302
|
+
- [ ] `word.count()`
|
|
303
|
+
- [ ] `word.length`
|
|
304
|
+
???
|
|
305
|
+
|
|
296
306
|
|
|
297
307
|
### Generic section
|
|
298
308
|
|
|
@@ -438,3 +448,4 @@ Here's a time range:
|
|
|
438
448
|
[flavored]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables
|
|
439
449
|
[presto]: https://github.com/abreen/presto
|
|
440
450
|
[shiki]: https://shiki.style/
|
|
451
|
+
[task-list]: https://github.github.com/gfm/#task-list-items-extension-
|
package/package.json
CHANGED
package/src/_base.scss
CHANGED
|
@@ -60,6 +60,19 @@ html {
|
|
|
60
60
|
background: var(--bg-color);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
.visually-hidden {
|
|
64
|
+
position: absolute !important;
|
|
65
|
+
width: 1px !important;
|
|
66
|
+
height: 1px !important;
|
|
67
|
+
padding: 0 !important;
|
|
68
|
+
margin: -1px !important;
|
|
69
|
+
overflow: hidden !important;
|
|
70
|
+
white-space: nowrap !important;
|
|
71
|
+
border: 0 !important;
|
|
72
|
+
clip: rect(0, 0, 0, 0) !important;
|
|
73
|
+
clip-path: inset(50%) !important;
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
a {
|
|
64
77
|
color: var(--link-color);
|
|
65
78
|
text-decoration-line: none;
|
package/src/question/index.ts
CHANGED
|
@@ -1,4 +1,64 @@
|
|
|
1
1
|
export default (window: Window) => {
|
|
2
|
+
const appendResultText = (option: HTMLElement, text: string) => {
|
|
3
|
+
const result = window.document.createElement('span');
|
|
4
|
+
result.className = 'visually-hidden question-multiple-choice-result';
|
|
5
|
+
result.textContent = text;
|
|
6
|
+
option.append(result);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const multipleChoiceBlocks = window.document.querySelectorAll<HTMLElement>(
|
|
10
|
+
'.question-multiple-choice',
|
|
11
|
+
);
|
|
12
|
+
multipleChoiceBlocks.forEach(block => {
|
|
13
|
+
const options = Array.from(
|
|
14
|
+
block.querySelectorAll<HTMLElement>('.question-multiple-choice-option'),
|
|
15
|
+
);
|
|
16
|
+
options.forEach(option => {
|
|
17
|
+
option.setAttribute('role', 'button');
|
|
18
|
+
option.setAttribute('tabindex', '0');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const reveal = (selected: HTMLElement) => {
|
|
22
|
+
block.setAttribute('data-revealed', '');
|
|
23
|
+
selected.setAttribute('data-selected', '');
|
|
24
|
+
options.forEach(option => {
|
|
25
|
+
option.removeAttribute('role');
|
|
26
|
+
option.removeAttribute('tabindex');
|
|
27
|
+
if (option.hasAttribute('data-correct')) {
|
|
28
|
+
appendResultText(
|
|
29
|
+
option,
|
|
30
|
+
option === selected ? 'Selected answer, correct' : 'Correct answer',
|
|
31
|
+
);
|
|
32
|
+
} else if (option === selected) {
|
|
33
|
+
appendResultText(option, 'Selected answer, incorrect');
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
options.forEach(option => {
|
|
39
|
+
option.addEventListener('click', (e: MouseEvent) => {
|
|
40
|
+
if (block.hasAttribute('data-revealed')) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
e.stopPropagation();
|
|
46
|
+
reveal(option);
|
|
47
|
+
});
|
|
48
|
+
option.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
49
|
+
if (block.hasAttribute('data-revealed')) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
reveal(option);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
2
62
|
const bodies =
|
|
3
63
|
window.document.querySelectorAll<HTMLElement>('.question-a-body');
|
|
4
64
|
bodies.forEach(el => {
|
package/src/question/style.scss
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
@use '../mixins' as *;
|
|
2
2
|
|
|
3
3
|
.question {
|
|
4
|
-
|
|
4
|
+
--question-label-width: 1.2em;
|
|
5
|
+
|
|
6
|
+
margin-bottom: 1rem;
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
.question-q,
|
|
@@ -17,7 +19,7 @@
|
|
|
17
19
|
|
|
18
20
|
.question-label,
|
|
19
21
|
.question-a-label {
|
|
20
|
-
flex
|
|
22
|
+
flex: 0 0 var(--question-label-width);
|
|
21
23
|
margin: 0;
|
|
22
24
|
font-weight: 700;
|
|
23
25
|
color: var(--theme-color-text);
|
|
@@ -32,17 +34,20 @@
|
|
|
32
34
|
|
|
33
35
|
.question-a-body {
|
|
34
36
|
flex: 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.js .question-a-body {
|
|
35
40
|
cursor: pointer;
|
|
36
41
|
|
|
37
42
|
@include user-select(none);
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
.question-a-body:focus-visible {
|
|
45
|
+
.js .question-a-body:focus-visible {
|
|
41
46
|
outline: 2px solid var(--theme-color);
|
|
42
47
|
outline-offset: -2px;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
.question-a-body[data-revealed] {
|
|
50
|
+
.js .question-a-body[data-revealed] {
|
|
46
51
|
&:focus,
|
|
47
52
|
&:focus-visible {
|
|
48
53
|
outline: none;
|
|
@@ -68,7 +73,7 @@
|
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
.question-a-body:not([data-revealed]) > * {
|
|
76
|
+
.js .question-a-body:not([data-revealed]) > * {
|
|
72
77
|
color: transparent;
|
|
73
78
|
pointer-events: none;
|
|
74
79
|
background-color: var(--bg2-color);
|
|
@@ -76,14 +81,14 @@
|
|
|
76
81
|
box-shadow: none;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
.question-a-body:not([data-revealed]) > * * {
|
|
84
|
+
.js .question-a-body:not([data-revealed]) > * * {
|
|
80
85
|
color: transparent;
|
|
81
86
|
background-color: var(--bg2-color);
|
|
82
87
|
border-color: transparent;
|
|
83
88
|
box-shadow: none;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
.question-a-body::after {
|
|
91
|
+
.js .question-a-body::after {
|
|
87
92
|
display: block;
|
|
88
93
|
font-size: var(--font-size-small);
|
|
89
94
|
font-style: italic;
|
|
@@ -97,22 +102,120 @@
|
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
.question-a-body[data-revealed]::after {
|
|
105
|
+
.js .question-a-body[data-revealed]::after {
|
|
101
106
|
opacity: 0;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
@media (hover: none) {
|
|
105
|
-
.question-a-body::after {
|
|
110
|
+
.js .question-a-body::after {
|
|
106
111
|
content: 'Tap to reveal';
|
|
107
112
|
}
|
|
108
113
|
}
|
|
109
114
|
|
|
110
|
-
.question-a-body[data-revealed] {
|
|
115
|
+
.js .question-a-body[data-revealed] {
|
|
111
116
|
cursor: unset;
|
|
112
117
|
|
|
113
118
|
@include user-select(text);
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
.question-multiple-choice {
|
|
122
|
+
--question-correct-color: hsl(142deg 58% 36%);
|
|
123
|
+
--question-correct-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='black' stroke-width='1.5'/><path d='M7 12.5 10.5 16 17 8.5' fill='none' stroke='black' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
|
124
|
+
--question-incorrect-color: hsl(0deg 72% 48%);
|
|
125
|
+
--question-incorrect-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M7 7 17 17 M17 7 7 17' fill='none' stroke='black' stroke-width='1.75' stroke-linecap='round'/></svg>");
|
|
126
|
+
--question-result-icon-size: 1.35em;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@media (prefers-color-scheme: dark) {
|
|
130
|
+
.question-multiple-choice {
|
|
131
|
+
--question-correct-color: hsl(142deg 58% 52%);
|
|
132
|
+
--question-incorrect-color: hsl(0deg 82% 62%);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.question-multiple-choice-options {
|
|
137
|
+
display: flex;
|
|
138
|
+
flex-direction: column;
|
|
139
|
+
gap: 0.5em;
|
|
140
|
+
margin-left: calc(var(--question-label-width) + 0.5em);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.question-multiple-choice-option {
|
|
144
|
+
position: relative;
|
|
145
|
+
padding: 0.25em calc(var(--question-result-icon-size) + 1em) 0.25em 0.5em;
|
|
146
|
+
cursor: pointer;
|
|
147
|
+
border: 0.125em solid var(--bg2-color);
|
|
148
|
+
border-radius: var(--border-radius);
|
|
149
|
+
|
|
150
|
+
@include user-select(none);
|
|
151
|
+
|
|
152
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
153
|
+
transition:
|
|
154
|
+
border-color 0.25s,
|
|
155
|
+
box-shadow 0.25s;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.question-multiple-choice-option:has(> code):not(
|
|
160
|
+
:has(> :not(code, .question-multiple-choice-result))
|
|
161
|
+
)
|
|
162
|
+
> code {
|
|
163
|
+
padding: 0;
|
|
164
|
+
background: none;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.question-multiple-choice-option::after {
|
|
168
|
+
position: absolute;
|
|
169
|
+
top: 50%;
|
|
170
|
+
right: 0.5em;
|
|
171
|
+
display: none;
|
|
172
|
+
width: var(--question-result-icon-size);
|
|
173
|
+
height: var(--question-result-icon-size);
|
|
174
|
+
pointer-events: none;
|
|
175
|
+
content: '';
|
|
176
|
+
transform: translateY(-50%);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.question-multiple-choice-option:focus-visible {
|
|
180
|
+
outline: 2px solid var(--theme-color);
|
|
181
|
+
outline-offset: 2px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.question-multiple-choice[data-revealed] {
|
|
185
|
+
.question-multiple-choice-option {
|
|
186
|
+
cursor: unset;
|
|
187
|
+
|
|
188
|
+
@include user-select(text);
|
|
189
|
+
|
|
190
|
+
&:focus,
|
|
191
|
+
&:focus-visible {
|
|
192
|
+
outline: none;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.question-multiple-choice-option[data-correct] {
|
|
197
|
+
border-color: var(--question-correct-color);
|
|
198
|
+
|
|
199
|
+
&::after {
|
|
200
|
+
display: block;
|
|
201
|
+
color: var(--question-correct-color);
|
|
202
|
+
|
|
203
|
+
@include contained-mask(var(--question-correct-icon));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.question-multiple-choice-option[data-selected]:not([data-correct]) {
|
|
208
|
+
border-color: var(--question-incorrect-color);
|
|
209
|
+
|
|
210
|
+
&::after {
|
|
211
|
+
display: block;
|
|
212
|
+
color: var(--question-incorrect-color);
|
|
213
|
+
|
|
214
|
+
@include contained-mask(var(--question-incorrect-icon));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
116
219
|
@media print {
|
|
117
220
|
.question-a-body {
|
|
118
221
|
cursor: default;
|
|
@@ -134,4 +237,21 @@
|
|
|
134
237
|
display: none;
|
|
135
238
|
}
|
|
136
239
|
}
|
|
240
|
+
|
|
241
|
+
.question-multiple-choice-option {
|
|
242
|
+
cursor: default;
|
|
243
|
+
|
|
244
|
+
@include user-select(text);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.question-multiple-choice-option[data-correct] {
|
|
248
|
+
border-color: var(--question-correct-color);
|
|
249
|
+
|
|
250
|
+
&::after {
|
|
251
|
+
display: block;
|
|
252
|
+
color: var(--question-correct-color);
|
|
253
|
+
|
|
254
|
+
@include contained-mask(var(--question-correct-icon));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
137
257
|
}
|
package/src/slides/index.ts
CHANGED
|
@@ -224,11 +224,21 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
function scrollToSlide(index: number): void {
|
|
228
|
+
const slide = slides[index];
|
|
229
|
+
if (!slide || typeof slide.scrollIntoView !== 'function') {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
slide.scrollIntoView({ block: 'start', inline: 'nearest' });
|
|
234
|
+
}
|
|
235
|
+
|
|
227
236
|
function exitPresentation(): void {
|
|
228
237
|
if (!isPresenting) {
|
|
229
238
|
return;
|
|
230
239
|
}
|
|
231
240
|
|
|
241
|
+
const exitingActiveIndex = activeIndex;
|
|
232
242
|
const shouldExitFullscreen =
|
|
233
243
|
presentationMode === 'fullscreen' &&
|
|
234
244
|
domDocument.fullscreenElement != null &&
|
|
@@ -245,8 +255,16 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
245
255
|
setTraceToolbarVisibility(false);
|
|
246
256
|
|
|
247
257
|
if (shouldExitFullscreen) {
|
|
248
|
-
void domDocument
|
|
258
|
+
void domDocument
|
|
259
|
+
.exitFullscreen()
|
|
260
|
+
.finally(() => scrollToSlide(exitingActiveIndex))
|
|
261
|
+
.catch(() => {
|
|
262
|
+
// ignored
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
249
265
|
}
|
|
266
|
+
|
|
267
|
+
scrollToSlide(exitingActiveIndex);
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
function resetReadyTraces(scope: HTMLElement = slidesRoot): void {
|
|
@@ -440,15 +458,7 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
440
458
|
return;
|
|
441
459
|
}
|
|
442
460
|
|
|
443
|
-
|
|
444
|
-
if (!activeSlide) {
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (
|
|
449
|
-
!(event.target instanceof domView.Node) ||
|
|
450
|
-
!activeSlide.contains(event.target)
|
|
451
|
-
) {
|
|
461
|
+
if (!(event.target instanceof domView.Node)) {
|
|
452
462
|
return;
|
|
453
463
|
}
|
|
454
464
|
|
package/src/slides/style.scss
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
body.is-presenting {
|
|
9
|
-
--slides-scale: clamp(150%,
|
|
9
|
+
--slides-scale: clamp(150%, 2.6vmin, 200%);
|
|
10
10
|
|
|
11
11
|
overflow: hidden;
|
|
12
12
|
}
|
|
@@ -55,20 +55,20 @@ body.is-presenting .slide {
|
|
|
55
55
|
@include user-select(none);
|
|
56
56
|
|
|
57
57
|
position: fixed;
|
|
58
|
-
top:
|
|
58
|
+
top: 0;
|
|
59
59
|
left: 50%;
|
|
60
60
|
z-index: 1000;
|
|
61
61
|
box-sizing: border-box;
|
|
62
62
|
display: none;
|
|
63
|
-
width: min(
|
|
64
|
-
height:
|
|
63
|
+
width: min(calc(100vw - var(--gap) - var(--gap)), 70rem);
|
|
64
|
+
height: 100vh;
|
|
65
65
|
padding: calc(3 * var(--gap));
|
|
66
66
|
overflow: auto;
|
|
67
67
|
font-size: var(--slides-scale);
|
|
68
68
|
letter-spacing: 0.2px;
|
|
69
69
|
outline: none;
|
|
70
70
|
background: var(--bg-color);
|
|
71
|
-
transform:
|
|
71
|
+
transform: translateX(-50%);
|
|
72
72
|
|
|
73
73
|
> :first-child {
|
|
74
74
|
margin-top: 0;
|
package/templates/_theme.scss
CHANGED
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
// Mask icons (color-agnostic, painted via background-color: currentColor)
|
|
58
58
|
--icon-external-link: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><path d='M6 18 L20 4 M10 4 H20 V14' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
59
59
|
--icon-anchor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 28'><path d='M11 4 L8 20 M17 4 L14 20 M5 9 H21 M3 15 H19' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
60
|
-
--icon-warning: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><
|
|
61
|
-
--icon-note: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='
|
|
60
|
+
--icon-warning: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M10.8 4.2 Q12 2 13.2 4.2 L20.8 17.9 Q22 20 19.6 20 H4.4 Q2 20 3.2 17.9 Z' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='black' stroke-width='2' stroke-linecap='square'/><line x1='12' y1='16' x2='12' y2='16' stroke='black' stroke-width='2' stroke-linecap='square'/></svg>");
|
|
61
|
+
--icon-note: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='9.5' fill='none' stroke='black' stroke-width='1.5'/><line x1='12' y1='10' x2='12' y2='16' stroke='black' stroke-width='2' stroke-linecap='square'/><line x1='12' y1='7' x2='12' y2='7' stroke='black' stroke-width='2' stroke-linecap='square'/></svg>");
|
|
62
62
|
--icon-check: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><path d='M5 12.5 10 17 19 7' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
63
63
|
--icon-search: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><circle cx='11' cy='11' r='7' stroke='<%= iconColor %>' stroke-width='2' fill='none'/><line x1='16.5' y1='16.5' x2='22' y2='22' stroke='<%= iconColor %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
64
64
|
--icon-chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><polyline points='9 18 15 12 9 6' stroke='<%= iconColor %>' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
package/templates/_top.html
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
<meta property="og:author" content="<%= page.author.name %>" />
|
|
34
34
|
<% } %>
|
|
35
35
|
<title><%= page.title %><%= site.titlePostfix %></title>
|
|
36
|
+
<script>document.documentElement.classList.add('js')</script>
|
|
36
37
|
<% if (!isWatchMode) { %>
|
|
37
38
|
<script type="speculationrules">
|
|
38
39
|
{
|
|
@@ -47,10 +48,9 @@
|
|
|
47
48
|
<% } %>
|
|
48
49
|
<script>document.head.appendChild(document.createElement('style')).textContent='.time-zone-label[hidden],select.time-zone[hidden]{display:revert}'</script>
|
|
49
50
|
<noscript><style>
|
|
50
|
-
.question-
|
|
51
|
-
.question-
|
|
52
|
-
.question-
|
|
53
|
-
.question-a-body { cursor: unset; }
|
|
51
|
+
.question-multiple-choice-option { cursor: unset; }
|
|
52
|
+
.question-multiple-choice-option[data-correct] { border-color: hsl(142deg 58% 36%); }
|
|
53
|
+
.question-multiple-choice-option[data-correct]::after { display: block; background-color: hsl(142deg 58% 36%); -webkit-mask-image: var(--question-correct-icon); mask-image: var(--question-correct-icon); -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-position: center center; mask-position: center center; -webkit-mask-size: contain; mask-size: contain; }
|
|
54
54
|
</style></noscript>
|
|
55
55
|
</head>
|
|
56
56
|
|