Skip to content

Commit 69aeaec

Browse files
committed
adapt tutorial components for shared use
1 parent 8e71df0 commit 69aeaec

File tree

5 files changed

+91
-43
lines changed

5 files changed

+91
-43
lines changed

packages/site_shared/lib/extensions/code_block_processor.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ final class CodeBlockProcessor implements PageExtension {
2121
static final opal.LanguageRegistry _languageRegistry =
2222
opal.LanguageRegistry.withDefaults();
2323

24-
const CodeBlockProcessor();
24+
const CodeBlockProcessor({required this.defaultTitle});
25+
26+
final String defaultTitle;
2527

2628
@override
2729
Future<List<Node>> apply(Page page, List<Node> nodes) async {
@@ -55,7 +57,7 @@ final class CodeBlockProcessor implements PageExtension {
5557
return ComponentNode(
5658
DartPadWrapper(
5759
content: lines.join('\n'),
58-
title: title ?? 'Runnable Flutter example',
60+
title: title ?? defaultTitle,
5961
theme: metadata['theme'],
6062
height: metadata['height'],
6163
runAutomatically: metadata['run'] == 'true',

packages/site_shared/lib/tutorial/client/quiz.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class _InteractiveQuizState extends State<InteractiveQuiz> {
8989
if (question == currentQuestion) 'active',
9090
].toClasses,
9191
[
92-
strong([.text(question.question)]),
92+
strong([RawText(question.question)]),
9393
ol([
9494
for (final (index, option) in question.options.indexed)
9595
li(
@@ -113,14 +113,14 @@ class _InteractiveQuizState extends State<InteractiveQuiz> {
113113
[
114114
div(classes: 'question-wrapper', [
115115
div(classes: 'question', [
116-
p([.text(option.text)]),
116+
p([RawText(option.text)]),
117117
]),
118118
div(classes: 'solution', [
119119
if (option.correct)
120120
const p(classes: 'correct', [.text('That\'s right!')])
121121
else
122-
const p(classes: 'incorrect', [.text('Not quite')]),
123-
p([.text(option.explanation)]),
122+
const p(classes: 'incorrect', [.text('Not quite.')]),
123+
p([RawText(option.explanation)]),
124124
]),
125125
]),
126126
],
@@ -144,7 +144,7 @@ class _InteractiveQuizState extends State<InteractiveQuiz> {
144144
currentQuestionIndex--;
145145
});
146146
},
147-
content: 'Previous',
147+
content: 'Previous question',
148148
),
149149
Button(
150150
key: nextButtonKey,

packages/site_shared/lib/tutorial/quiz.dart

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,68 @@ import 'package:jaspr/jaspr.dart';
66
import 'package:jaspr_content/jaspr_content.dart';
77
import 'package:yaml/yaml.dart';
88

9-
import 'models/quiz_model.dart';
9+
import '../markdown/markdown_parser.dart';
1010
import 'client/quiz.dart';
11+
import 'models/quiz_model.dart';
1112

1213
class Quiz extends CustomComponent {
1314
const Quiz() : super.base();
1415

1516
@override
1617
Component? create(Node node, NodesBuilder builder) {
17-
if (node is ElementNode && node.tag.toLowerCase() == 'quiz') {
18-
if (node.children?.whereType<ElementNode>().isNotEmpty ?? false) {
19-
throw Exception(
20-
'Invalid Quiz content. Remove any leading empty lines to '
21-
'avoid parsing as markdown.',
22-
);
23-
}
24-
25-
final title = node.attributes['title'];
26-
27-
final content = node.children?.map((n) => n.innerText).join('\n') ?? '';
28-
final data = loadYamlNode(content);
29-
assert(data is YamlList, 'Invalid Quiz content. Expected a YAML list.');
30-
final questions = (data as YamlList).nodes
31-
.map((n) => Question.fromMap(n as YamlMap))
32-
.toList();
33-
assert(questions.isNotEmpty, 'Quiz must contain at least one question.');
34-
return InteractiveQuiz(title: title, questions: questions);
18+
if (node is! ElementNode || node.tag.toLowerCase() != 'quiz') {
19+
return null;
20+
}
21+
22+
final title = node.attributes['title'];
23+
24+
// If the quiz has an ID, load it from the page data.
25+
if (node.attributes['id'] case final String quizId when quizId.isNotEmpty) {
26+
return Builder(
27+
builder: (context) {
28+
final quizzes = context.page.data['quiz'] as Map<String, Object?>?;
29+
if (quizzes?[quizId] case final List<Object?> quizData) {
30+
return InteractiveQuiz(
31+
title: title,
32+
questions: quizData
33+
.map((q) => _parseQuestion(q as Map<String, Object?>))
34+
.toList(growable: false),
35+
);
36+
}
37+
38+
throw ArgumentError('Failed to parse quiz with ID: $quizId');
39+
},
40+
);
3541
}
36-
return null;
42+
43+
// If the quiz does not have an ID, parse it from the content.
44+
if (node.children?.whereType<ElementNode>().isNotEmpty ?? false) {
45+
throw Exception(
46+
'Invalid Quiz content. Remove any leading empty lines to '
47+
'avoid parsing as markdown.',
48+
);
49+
}
50+
51+
final content = node.children?.map((n) => n.innerText).join('\n') ?? '';
52+
final data = loadYamlNode(content);
53+
assert(data is YamlList, 'Invalid Quiz content. Expected a YAML list.');
54+
final questions = (data as YamlList).nodes
55+
.map((n) => Question.fromMap(n as YamlMap))
56+
.toList();
57+
assert(questions.isNotEmpty, 'Quiz must contain at least one question.');
58+
return InteractiveQuiz(title: title, questions: questions);
3759
}
3860
}
61+
62+
Question _parseQuestion(Map<Object?, Object?> map) => Question(
63+
parseMarkdownToHtml(map['question'] as String, inline: true),
64+
(map['options'] as List<Object?>)
65+
.map((e) => _parseAnswer(e as Map<Object?, Object?>))
66+
.toList(),
67+
);
68+
69+
AnswerOption _parseAnswer(Map<Object?, Object?> map) => AnswerOption(
70+
parseMarkdownToHtml(map['text'] as String, inline: true),
71+
map['correct'] as bool? ?? false,
72+
parseMarkdownToHtml(map['explanation'] as String),
73+
);

packages/site_shared/lib/tutorial/tutorial_outline.dart

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import '../markdown/markdown_parser.dart';
1010
import 'models/tutorial_model.dart';
1111

1212
class TutorialOutline extends CustomComponentBase {
13-
const TutorialOutline();
13+
const TutorialOutline({this.showUnitTitle = true});
14+
15+
final bool showUnitTitle;
1416

1517
@override
1618
Pattern get pattern => 'TutorialOutline';
@@ -30,22 +32,31 @@ class TutorialOutline extends CustomComponentBase {
3032
};
3133

3234
return div(classes: 'tutorial-outline', [
33-
ol([
34-
for (final unit in model.units)
35-
li([
36-
.text(unit.title),
37-
ol([
38-
for (final chapter in unit.chapters)
39-
li([
40-
a(href: chapter.url, [
41-
DashMarkdown(content: chapter.title, inline: true),
42-
]),
43-
]),
44-
]),
45-
]),
46-
]),
35+
ol([for (final unit in model.units) ..._buildUnit(unit)]),
4736
]);
4837
},
4938
);
5039
}
40+
41+
List<Component> _buildUnit(TutorialUnit unit) {
42+
final chapters = [
43+
for (final chapter in unit.chapters)
44+
li([
45+
a(href: chapter.url, [
46+
DashMarkdown(content: chapter.title, inline: true),
47+
]),
48+
]),
49+
];
50+
51+
if (showUnitTitle) {
52+
return [
53+
li([
54+
.text(unit.title),
55+
ol(chapters),
56+
]),
57+
];
58+
} else {
59+
return chapters;
60+
}
61+
}
5162
}

site/lib/src/extensions/registry.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const List<PageExtension> allNodeProcessingExtensions = [
2020
HeaderExtractorExtension(),
2121
HeaderWrapperExtension(),
2222
TableWrapperExtension(),
23-
CodeBlockProcessor(),
23+
CodeBlockProcessor(defaultTitle: 'Runnable Flutter example'),
2424
GlossaryLinkProcessor(),
2525
TutorialNavigationExtension(),
2626
TutorialStructureExtension(),

0 commit comments

Comments
 (0)