@lernapps/app-templates 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +195 -2
  3. package/dist/appname-Bh23cUuz.mjs +12 -0
  4. package/dist/assessment-By2GGTJC.mjs +1696 -0
  5. package/dist/block-BK_ohsH1.mjs +82 -0
  6. package/dist/blocks-BmRrD7UY.mjs +149 -0
  7. package/dist/blocks.mjs +4 -0
  8. package/dist/check-BkEl-m-F.mjs +254 -0
  9. package/dist/content-cYBPYg_J.mjs +140 -0
  10. package/dist/contract-PONpxJtd.mjs +52 -0
  11. package/dist/e2e-BiMI-pCL.mjs +89 -0
  12. package/dist/engine-BqF2Vffn.mjs +136 -0
  13. package/dist/explainer-e2e.mjs +1297 -0
  14. package/dist/explainer-islands.mjs +197 -0
  15. package/dist/explainer-plugin.mjs +2131 -0
  16. package/dist/explainer.mjs +1246 -0
  17. package/dist/html-mffkHGHh.mjs +89 -0
  18. package/dist/image-CspJVfyQ.mjs +21 -0
  19. package/dist/interactive-e2e.mjs +140 -0
  20. package/dist/interactive-plugin.mjs +242 -0
  21. package/dist/interactive.mjs +75 -0
  22. package/dist/load-C-0hUaSC.mjs +16947 -0
  23. package/dist/needs-BzEahJ6X.mjs +97 -0
  24. package/dist/pages-9RaaomoO.mjs +81 -0
  25. package/dist/questions-e2e.mjs +1 -0
  26. package/dist/questions-engine.mjs +3 -0
  27. package/dist/questions.mjs +6 -0
  28. package/dist/quiz-BKc8JYtC.mjs +70 -0
  29. package/dist/quiz-e2e.mjs +174 -0
  30. package/dist/quiz-plugin.mjs +266 -0
  31. package/dist/quiz.mjs +246 -0
  32. package/dist/render-BJey0bwl.mjs +540 -0
  33. package/dist/render-BL6fV_NI.mjs +507 -0
  34. package/dist/render-DpRO_RKm.mjs +146 -0
  35. package/dist/result-_eQcamoz.mjs +180 -0
  36. package/dist/tasks-DADUYy52.mjs +74 -0
  37. package/dist/validate-COaZso7u.mjs +1072 -0
  38. package/dist/validate-CxtdeQ5S.mjs +146 -0
  39. package/dist/vue-DO1m6q1R.mjs +40 -0
  40. package/dist/vue-code-BkJjW_I_.mjs +320 -0
  41. package/docs/arc42/01-introduction-and-goals.arc42.md +82 -0
  42. package/docs/arc42/02-constraints.arc42.md +228 -0
  43. package/docs/arc42/03-system-context.arc42.md +118 -0
  44. package/docs/arc42/04-solution-strategy.arc42.md +47 -0
  45. package/docs/arc42/05-building-blocks.arc42.md +307 -0
  46. package/docs/arc42/06-runtime-view.arc42.md +98 -0
  47. package/docs/arc42/07-deployment-view.arc42.md +160 -0
  48. package/docs/arc42/08-concepts.arc42.md +123 -0
  49. package/docs/arc42/09-decisions.arc42.md +246 -0
  50. package/docs/arc42/10-quality-requirements.arc42.md +155 -0
  51. package/docs/arc42/11-risks.arc42.md +113 -0
  52. package/docs/arc42/12-glossary.arc42.md +162 -0
  53. package/docs/arc42/architecture-evidence.md +395 -0
  54. package/explainer/.arc42ignore +3 -0
  55. package/explainer/README.md +54 -0
  56. package/explainer/_gitignore +5 -0
  57. package/explainer/docs/arc42/03-explainer-system-context.arc42.md +35 -0
  58. package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +659 -0
  59. package/explainer/docs/arc42/06-explainer-runtime-view.arc42.md +105 -0
  60. package/explainer/docs/arc42/07-explainer-deployment-view.arc42.md +34 -0
  61. package/explainer/docs/arc42/08-explainer-concepts.arc42.md +17 -0
  62. package/explainer/docs/arc42/09-explainer-decisions.arc42.md +443 -0
  63. package/explainer/docs/arc42/10-explainer-quality-requirements.arc42.md +54 -0
  64. package/explainer/docs/arc42/11-explainer-risks.arc42.md +17 -0
  65. package/explainer/docs/arc42/12-explainer-glossary.arc42.md +185 -0
  66. package/explainer/e2e/explainer.e2e.ts +3 -0
  67. package/explainer/index.html +11 -0
  68. package/explainer/package.json +18 -0
  69. package/explainer/playwright.config.ts +1 -0
  70. package/explainer/src/app.yaml +20 -0
  71. package/explainer/src/components/ProzentFeld.test.ts +30 -0
  72. package/explainer/src/components/ProzentFeld.ts +121 -0
  73. package/explainer/src/components/ProzentFeld.vue +84 -0
  74. package/explainer/src/images/hunderterfeld-klammern.svg +105 -0
  75. package/explainer/src/main.ts +4 -0
  76. package/explainer/src/prozent/chapter.yaml +18 -0
  77. package/explainer/src/prozent/grundwert.yaml +115 -0
  78. package/explainer/src/prozent/prozentsatz.yaml +99 -0
  79. package/explainer/src/prozent/prozentwert.yaml +119 -0
  80. package/explainer/tsconfig.json +1 -0
  81. package/explainer/vite.config.ts +4 -0
  82. package/interactive/.arc42ignore +5 -0
  83. package/interactive/README.md +28 -0
  84. package/interactive/_gitignore +4 -0
  85. package/interactive/docs/arc42/05-interactive-building-blocks.arc42.md +518 -0
  86. package/interactive/docs/arc42/06-interactive-runtime-view.arc42.md +58 -0
  87. package/interactive/docs/arc42/07-interactive-deployment-view.arc42.md +35 -0
  88. package/interactive/docs/arc42/08-interactive-concepts.arc42.md +52 -0
  89. package/interactive/docs/arc42/09-interactive-decisions.arc42.md +130 -0
  90. package/interactive/docs/arc42/10-interactive-quality-requirements.arc42.md +45 -0
  91. package/interactive/docs/arc42/11-interactive-risks.arc42.md +20 -0
  92. package/interactive/docs/arc42/12-interactive-glossary.arc42.md +80 -0
  93. package/interactive/e2e/interactive.e2e.ts +3 -0
  94. package/interactive/index.html +11 -0
  95. package/interactive/package.json +18 -0
  96. package/interactive/playwright.config.ts +1 -0
  97. package/interactive/src/app.yaml +28 -0
  98. package/interactive/src/core/index.ts +101 -0
  99. package/interactive/src/data/lessons.json +4 -0
  100. package/interactive/src/env.d.ts +6 -0
  101. package/interactive/src/main.ts +6 -0
  102. package/interactive/src/messages/de.json +12 -0
  103. package/interactive/src/playthroughs/grundreihe.yaml +33 -0
  104. package/interactive/src/playthroughs/obere-reihe.yaml +14 -0
  105. package/interactive/src/ui/App.vue +85 -0
  106. package/interactive/tsconfig.json +1 -0
  107. package/interactive/vite.config.ts +4 -0
  108. package/package.json +144 -4
  109. package/quiz/README.md +31 -0
  110. package/quiz/_gitignore +5 -0
  111. package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +283 -0
  112. package/quiz/docs/arc42/06-quiz-runtime-view.arc42.md +21 -0
  113. package/quiz/docs/arc42/07-quiz-deployment-view.arc42.md +33 -0
  114. package/quiz/docs/arc42/09-quiz-decisions.arc42.md +36 -0
  115. package/quiz/docs/arc42/10-quiz-quality-requirements.arc42.md +53 -0
  116. package/quiz/docs/arc42/11-quiz-risks.arc42.md +19 -0
  117. package/quiz/docs/arc42/12-quiz-glossary.arc42.md +27 -0
  118. package/quiz/e2e/quiz.e2e.ts +3 -0
  119. package/quiz/index.html +11 -0
  120. package/quiz/package.json +18 -0
  121. package/quiz/playwright.config.ts +1 -0
  122. package/quiz/src/images/berge-ordnen.jpg +0 -0
  123. package/quiz/src/images/everest-hoehe.jpg +0 -0
  124. package/quiz/src/images/galapagos.jpg +0 -0
  125. package/quiz/src/images/grand-canyon.jpg +0 -0
  126. package/quiz/src/images/totes-meer.jpg +0 -0
  127. package/quiz/src/images/uluru.jpg +0 -0
  128. package/quiz/src/images/wasserfaelle-laender.jpg +0 -0
  129. package/quiz/src/main.ts +4 -0
  130. package/quiz/src/quiz.json +312 -0
  131. package/quiz/tsconfig.json +1 -0
  132. package/quiz/vite.config.ts +4 -0
  133. package/runtime/explainer/assessment-client.ts +478 -0
  134. package/runtime/explainer/assessment-page.ts +446 -0
  135. package/runtime/explainer/assessment.ts +393 -0
  136. package/runtime/explainer/check-page.ts +520 -0
  137. package/runtime/explainer/components/exercise.ts +409 -0
  138. package/runtime/explainer/components/props.ts +230 -0
  139. package/runtime/explainer/components/read.ts +428 -0
  140. package/runtime/explainer/content.ts +277 -0
  141. package/runtime/explainer/e2e.ts +1474 -0
  142. package/runtime/explainer/exercises.ts +324 -0
  143. package/runtime/explainer/explainer.css +1283 -0
  144. package/runtime/explainer/explainer.ts +838 -0
  145. package/runtime/explainer/explainer.v1.schema.json +1165 -0
  146. package/runtime/explainer/expression.ts +407 -0
  147. package/runtime/explainer/fraction.ts +107 -0
  148. package/runtime/explainer/generated/comparator.ts +31 -0
  149. package/runtime/explainer/generated/question.ts +165 -0
  150. package/runtime/explainer/generated/soundness.ts +280 -0
  151. package/runtime/explainer/generated/texts.ts +257 -0
  152. package/runtime/explainer/generated/types.ts +103 -0
  153. package/runtime/explainer/generated/values.ts +136 -0
  154. package/runtime/explainer/islands.ts +281 -0
  155. package/runtime/explainer/load.ts +457 -0
  156. package/runtime/explainer/messages/de.json +200 -0
  157. package/runtime/explainer/messages/en.json +200 -0
  158. package/runtime/explainer/messages.ts +13 -0
  159. package/runtime/explainer/needs.ts +122 -0
  160. package/runtime/explainer/open-task.ts +235 -0
  161. package/runtime/explainer/outline.ts +299 -0
  162. package/runtime/explainer/plugin.ts +488 -0
  163. package/runtime/explainer/position.ts +62 -0
  164. package/runtime/explainer/prompt.ts +75 -0
  165. package/runtime/explainer/render.ts +686 -0
  166. package/runtime/explainer/result.ts +238 -0
  167. package/runtime/explainer/routes.ts +119 -0
  168. package/runtime/explainer/schema.ts +140 -0
  169. package/runtime/explainer/scope.ts +87 -0
  170. package/runtime/explainer/stuck.ts +57 -0
  171. package/runtime/explainer/tasks.ts +109 -0
  172. package/runtime/explainer/tutor.ts +469 -0
  173. package/runtime/explainer/tutorpage.ts +669 -0
  174. package/runtime/explainer/video.ts +123 -0
  175. package/runtime/interactive/contract.ts +121 -0
  176. package/runtime/interactive/e2e.ts +190 -0
  177. package/runtime/interactive/interactive.css +82 -0
  178. package/runtime/interactive/interactive.ts +115 -0
  179. package/runtime/interactive/interactive.v1.schema.json +146 -0
  180. package/runtime/interactive/load.ts +188 -0
  181. package/runtime/interactive/messages/de.json +14 -0
  182. package/runtime/interactive/messages/en.json +14 -0
  183. package/runtime/interactive/messages.ts +11 -0
  184. package/runtime/interactive/playthrough.v1.schema.json +143 -0
  185. package/runtime/interactive/plugin.ts +266 -0
  186. package/runtime/interactive/render.ts +126 -0
  187. package/runtime/quiz/e2e.ts +233 -0
  188. package/runtime/quiz/engine.ts +242 -0
  189. package/runtime/quiz/messages/de.json +23 -0
  190. package/runtime/quiz/messages/en.json +23 -0
  191. package/runtime/quiz/plugin.ts +150 -0
  192. package/runtime/quiz/quiz.css +154 -0
  193. package/runtime/quiz/quiz.ts +56 -0
  194. package/runtime/quiz/quiz.v1.schema.json +72 -0
  195. package/runtime/quiz/render.ts +173 -0
  196. package/runtime/quiz/validate.ts +108 -0
  197. package/runtime/shared/appname.ts +16 -0
  198. package/runtime/shared/blocks/block.ts +162 -0
  199. package/runtime/shared/blocks/blocks.css +190 -0
  200. package/runtime/shared/blocks/blocks.v1.schema.json +432 -0
  201. package/runtime/shared/blocks/code.ts +26 -0
  202. package/runtime/shared/blocks/figures.ts +49 -0
  203. package/runtime/shared/blocks/image.ts +25 -0
  204. package/runtime/shared/blocks/index.ts +13 -0
  205. package/runtime/shared/blocks/messages/de.json +4 -0
  206. package/runtime/shared/blocks/messages/en.json +4 -0
  207. package/runtime/shared/blocks/render.ts +134 -0
  208. package/runtime/shared/blocks/validate.ts +165 -0
  209. package/runtime/shared/content.ts +197 -0
  210. package/runtime/shared/examples.ts +20 -0
  211. package/runtime/shared/html.ts +10 -0
  212. package/runtime/shared/languages.ts +94 -0
  213. package/runtime/shared/page.css +101 -0
  214. package/runtime/shared/pages.ts +114 -0
  215. package/runtime/shared/questions/e2e.ts +115 -0
  216. package/runtime/shared/questions/engine.ts +209 -0
  217. package/runtime/shared/questions/index.ts +39 -0
  218. package/runtime/shared/questions/messages/de.json +26 -0
  219. package/runtime/shared/questions/messages/en.json +26 -0
  220. package/runtime/shared/questions/question.ts +430 -0
  221. package/runtime/shared/questions/question.v1.schema.json +342 -0
  222. package/runtime/shared/questions/questions.css +192 -0
  223. package/runtime/shared/questions/render.ts +247 -0
  224. package/runtime/shared/questions/validate.ts +518 -0
  225. package/runtime/shared/schema.ts +44 -0
  226. package/runtime/shared/text/check.ts +68 -0
  227. package/runtime/shared/text/formula.ts +69 -0
  228. package/runtime/shared/text/markdown.ts +185 -0
  229. package/runtime/shared/text/text.v1.schema.json +34 -0
  230. package/runtime/shared/vue-code.ts +395 -0
  231. package/runtime/shared/vue.ts +38 -0
@@ -0,0 +1,324 @@
1
+ // The exercises of an explainer page at build time: a block of one of the question types, rendered through
2
+ // the per-question exports of the shared runtime (shared/questions) (its controls, and a <details> "Lösung" with the right answer, the
3
+ // background of every option, the explanation and the links); and a generated exercise: its templates rendered once
4
+ // per language (restricted Markdown, formulas as MathML, the placeholders as tokens), the place of the live task, and
5
+ // three worked tasks, the task numbers 1 to 3 (taskSeeds), each with its task number and a <details> "Lösung" (with JavaScript they show
6
+ // once the live task is solved). Each exercise carries the data its live part
7
+ // needs in the browser (explainer.ts): the question, or the exercise with its templates and data files.
8
+ import { questionIn, type Question } from "../shared/questions/question.ts";
9
+ import { renderMaterial, type MaterialContext } from "../shared/blocks/index.ts";
10
+ import { answerText, questionMessagesIn, renderQuestion, renderSolution } from "../shared/questions/render.ts";
11
+ import { inLanguage, type Text } from "../shared/languages.ts";
12
+ import type { DataRecord, Exercise, OpenTask, QuestionBlock } from "./content.ts";
13
+ import { hintOf, stepsOf, taskText, tokenize } from "./generated/texts.ts";
14
+ import { generate } from "./generated/values.ts";
15
+ import { questionFor, taskKey } from "./generated/question.ts";
16
+ import { WORKED_TASKS } from "./tasks.ts";
17
+ import { type Data, type Generated, type LanguageTemplates, type Templates } from "./generated/types.ts";
18
+ import { plain, renderInline, renderMarkdown, renderWithFormulas } from "../shared/text/markdown.ts";
19
+ import { distinctSeeds } from "./tasks.ts";
20
+ import { message } from "./messages.ts";
21
+
22
+ /** What the browser reads from an exercise (script.explainer-exercise-data). */
23
+ export type ExerciseData =
24
+ | {
25
+ kind: "question";
26
+ lang: string;
27
+ question: Question;
28
+ /** The texts of the question that hold a formula, as HTML (the feedback quotes them); the others are plain. */
29
+ rendered: Record<string, string>;
30
+ }
31
+ | {
32
+ kind: "generated";
33
+ lang: string;
34
+ spec: Generated;
35
+ templates: Templates;
36
+ data: Data;
37
+ /** The seed of each task number, from 1 (taskSeeds). */
38
+ seeds: number[];
39
+ texts: { newTask: string; practice: string; hint: string; solution: string; taskNumber: string };
40
+ }
41
+ | OpenTaskData;
42
+
43
+ /** What the browser reads from an open task: its texts as HTML, its points, how the learner may answer. */
44
+ export interface OpenTaskData {
45
+ kind: "open";
46
+ lang: string;
47
+ id: string;
48
+ /** The task as HTML. */
49
+ task: string;
50
+ answer: readonly ("text" | "photo")[];
51
+ /** Each point as plain text, its kind, and the address of its source on this page's tree. */
52
+ points: { text: string; kind: "content" | "form"; href?: string }[];
53
+ /** The model answer as HTML. */
54
+ solution: string;
55
+ /** The hint as HTML. */
56
+ hint?: string;
57
+ }
58
+
59
+ /** The templates of a generated exercise in each of `languages`; links inside them through `resolve`. */
60
+ export function templatesOf(
61
+ spec: Generated,
62
+ languages: readonly string[],
63
+ resolve: (href: string) => string = (href) => href,
64
+ ): Templates {
65
+ const templates: Record<string, LanguageTemplates> = {};
66
+ for (const lang of languages) {
67
+ const inline = (text: Text) => renderInline(tokenize(inLanguage(text, lang)), resolve);
68
+ templates[lang] = {
69
+ task: inline(spec.task.text),
70
+ steps: (spec.steps ?? []).map(inline),
71
+ hint: spec.hint === undefined ? undefined : inline(spec.hint),
72
+ messages: (spec.mistakes ?? []).map((mistake) => tokenize(inLanguage(mistake.message, lang))),
73
+ };
74
+ }
75
+ return templates;
76
+ }
77
+
78
+ /** The data files a generated exercise draws from, by name. */
79
+ export function dataOf(spec: Generated, all: Readonly<Record<string, readonly DataRecord[]>>): Data {
80
+ const names = Object.values(spec.given).flatMap((arbitrary) =>
81
+ arbitrary.from !== undefined && !Array.isArray(arbitrary.from) ? [(arbitrary.from as { data: string }).data] : [],
82
+ );
83
+ return Object.fromEntries(names.filter((name) => all[name] !== undefined).map((name) => [name, all[name] ?? []]));
84
+ }
85
+
86
+ const seedCache = new WeakMap<Generated, number[]>();
87
+
88
+ /**
89
+ * The seed of each task number of a generated exercise, from 1: the seeds of its distinct tasks (what the learner sees
90
+ * of a task, in German), up to MAX_TASKS (tasks.ts); once per exercise.
91
+ */
92
+ export function taskSeeds(spec: Generated, data: Data): number[] {
93
+ let seeds = seedCache.get(spec);
94
+ if (seeds === undefined) {
95
+ const templates = templatesOf(spec, ["de"]);
96
+ seeds = distinctSeeds((seed) => {
97
+ try {
98
+ return taskKey(spec, generate(spec, seed, data), "de", templates);
99
+ } catch {
100
+ return undefined;
101
+ }
102
+ });
103
+ seedCache.set(spec, seeds);
104
+ }
105
+ return seeds;
106
+ }
107
+
108
+ /** One worked task of a generated exercise: what the page shows without JavaScript, and what the tests expect. */
109
+ export interface Worked {
110
+ /** Its task number. */
111
+ number: number;
112
+ seed: number;
113
+ /** The task as HTML. */
114
+ task: string;
115
+ question: Question;
116
+ hint?: string;
117
+ steps: string[];
118
+ /** The right answer as the solution states it. */
119
+ answer: string;
120
+ }
121
+
122
+ /** The worked tasks of a generated exercise in `lang`. */
123
+ export function workedTasks(spec: Generated, data: Data, templates: Templates, lang: string): Worked[] {
124
+ return taskSeeds(spec, data)
125
+ .slice(0, WORKED_TASKS)
126
+ .map((seed, index) => {
127
+ const task = generate(spec, seed, data);
128
+ const question = questionFor(spec, task, lang, templates);
129
+ const hint = hintOf(spec, task, lang, templates);
130
+ return {
131
+ number: index + 1,
132
+ seed,
133
+ task: taskText(spec, task, lang, templates),
134
+ question,
135
+ steps: stepsOf(spec, task, lang, templates),
136
+ answer: answerText(question, lang),
137
+ ...(hint === undefined ? {} : { hint }),
138
+ };
139
+ });
140
+ }
141
+
142
+ const json = (value: unknown) => JSON.stringify(value).replace(/</g, "\\u003c");
143
+
144
+ /** The texts of a question that hold a formula, $…$. */
145
+ function formulaTexts(value: unknown): string[] {
146
+ if (typeof value === "string") return value.includes("$") && /\$[^$\n]+\$/.test(value) ? [value] : [];
147
+ if (Array.isArray(value)) return value.flatMap(formulaTexts);
148
+ if (typeof value === "object" && value !== null) return Object.values(value).flatMap(formulaTexts);
149
+ return [];
150
+ }
151
+
152
+ /** What the browser reads from a question of the question types in `lang`: the question, its formulas as HTML. */
153
+ function questionData(block: QuestionBlock, lang: string): Extract<ExerciseData, { kind: "question" }> {
154
+ const question = questionIn(block, lang);
155
+ const rendered: Record<string, string> = {};
156
+ for (const text of formulaTexts(question)) rendered[text] = renderWithFormulas(text);
157
+ return { kind: "question", lang, question, rendered };
158
+ }
159
+
160
+ function questionExercise(block: QuestionBlock, lang: string, material: MaterialContext): string {
161
+ const data = questionData(block, lang);
162
+ const { question } = data;
163
+ return [
164
+ `<div class="explainer-exercise explainer-question" id="${block.id}">`,
165
+ `<p class="explainer-exercise-label">${plain(message(lang, "exerciseLabel"))}</p>`,
166
+ renderQuestion(question, {
167
+ lang,
168
+ format: renderWithFormulas,
169
+ ...(question.material === undefined ? {} : { materialHtml: renderMaterial(question.material, material) }),
170
+ }),
171
+ `<details class="explainer-solution"><summary>${plain(questionMessagesIn(lang).solution)}</summary>`,
172
+ renderSolution(question, { lang, format: renderWithFormulas }),
173
+ `</details>`,
174
+ `<script type="application/json" class="explainer-exercise-data">${json(data)}</script>`,
175
+ `</div>`,
176
+ ].join("\n");
177
+ }
178
+
179
+ /** What the browser reads from a generated exercise in `lang`: the exercise, its templates, data files and seeds. */
180
+ function generatedData(
181
+ spec: Generated,
182
+ lang: string,
183
+ all: Readonly<Record<string, readonly DataRecord[]>>,
184
+ resolve: (href: string) => string,
185
+ ): Extract<ExerciseData, { kind: "generated" }> {
186
+ const data = dataOf(spec, all);
187
+ // the examples are test cases for the build: not shipped
188
+ const { examples: _examples, ...shipped } = spec;
189
+ return {
190
+ kind: "generated",
191
+ lang,
192
+ spec: shipped,
193
+ templates: templatesOf(spec, [lang], resolve),
194
+ data,
195
+ seeds: taskSeeds(spec, data),
196
+ texts: {
197
+ newTask: message(lang, "newTask"),
198
+ practice: spec.practice === undefined ? message(lang, "practice") : inLanguage(spec.practice, lang),
199
+ hint: message(lang, "hint"),
200
+ solution: questionMessagesIn(lang).solution,
201
+ // with {n} kept for the client module
202
+ taskNumber: message(lang, "taskNumber"),
203
+ },
204
+ };
205
+ }
206
+
207
+ /** What the browser reads from an open task in `lang`; links and sources through `resolve`. */
208
+ export function openTaskData(block: OpenTask, lang: string, resolve: (href: string) => string): OpenTaskData {
209
+ return {
210
+ kind: "open",
211
+ lang,
212
+ id: block.id,
213
+ task: renderMarkdown(inLanguage(block.task, lang), resolve),
214
+ answer: block.answer ?? ["text", "photo"],
215
+ points: block.points.map((point) => ({
216
+ text: inLanguage(point.text, lang),
217
+ kind: point.kind ?? "content",
218
+ ...(point.source === undefined ? {} : { href: resolve(point.source) }),
219
+ })),
220
+ solution: renderMarkdown(inLanguage(block.solution, lang), resolve),
221
+ ...(block.hint === undefined ? {} : { hint: renderMarkdown(inLanguage(block.hint, lang), resolve) }),
222
+ };
223
+ }
224
+
225
+ /** What the browser reads from an exercise in `lang` (script.explainer-exercise-data). */
226
+ export function exerciseData(
227
+ block: Exercise,
228
+ lang: string,
229
+ all: Readonly<Record<string, readonly DataRecord[]>>,
230
+ resolve: (href: string) => string,
231
+ ): ExerciseData {
232
+ if (block.type === "generated") return generatedData(block, lang, all, resolve);
233
+ if (block.type === "open-task") return openTaskData(block, lang, resolve);
234
+ return questionData(block, lang);
235
+ }
236
+
237
+ /**
238
+ * An open task in `lang`: the task, the points of a good answer with a link to each one's source, and the model answer
239
+ * under "Lösung", which is all the page shows without JavaScript; with JavaScript the learner answers first (explainer.ts).
240
+ */
241
+ function openTaskExercise(block: OpenTask, lang: string, resolve: (href: string) => string, storage: string): string {
242
+ const data = openTaskData(block, lang, resolve);
243
+ const points = data.points.map((point) => {
244
+ const where =
245
+ point.href === undefined
246
+ ? ` <span class="explainer-open-kind">(${plain(message(lang, "openTaskForm"))})</span>`
247
+ : ` <a class="explainer-open-source" href="${plain(point.href)}">${plain(message(lang, "openTaskSource"))}</a>`;
248
+ return `<li>${plain(point.text)}${where}</li>`;
249
+ });
250
+ return [
251
+ `<div class="explainer-exercise explainer-open-task" id="${block.id}" data-storage="${plain(storage)}">`,
252
+ `<p class="explainer-exercise-label">${plain(message(lang, "openTaskLabel"))}</p>`,
253
+ `<div class="explainer-open-task-text">${data.task}</div>`,
254
+ data.hint === undefined
255
+ ? ""
256
+ : `<details class="explainer-hint"><summary>${plain(message(lang, "hint"))}</summary>${data.hint}</details>`,
257
+ `<div class="explainer-open-live" hidden></div>`,
258
+ `<div class="explainer-open-points">`,
259
+ `<p class="explainer-open-points-intro">${plain(message(lang, "openTaskPoints"))}</p>`,
260
+ `<ul>${points.join("")}</ul>`,
261
+ `</div>`,
262
+ `<details class="explainer-solution"><summary>${plain(questionMessagesIn(lang).solution)}</summary>`,
263
+ `<div class="explainer-open-solution">${data.solution}</div>`,
264
+ `</details>`,
265
+ `<script type="application/json" class="explainer-exercise-data">${json(data)}</script>`,
266
+ `</div>`,
267
+ ]
268
+ .filter((part) => part !== "")
269
+ .join("\n");
270
+ }
271
+
272
+ function generatedExercise(
273
+ spec: Generated,
274
+ lang: string,
275
+ all: Readonly<Record<string, readonly DataRecord[]>>,
276
+ resolve: (href: string) => string,
277
+ ): string {
278
+ const templates = templatesOf(spec, [lang], resolve);
279
+ const data = dataOf(spec, all);
280
+ const solution = questionMessagesIn(lang).solution;
281
+ const worked = workedTasks(spec, data, templates, lang).map((item) =>
282
+ [
283
+ `<li class="explainer-worked-task">`,
284
+ `<p class="explainer-task-number">${plain(message(lang, "taskNumber", { n: String(item.number) }))}</p>`,
285
+ `<p class="explainer-task">${item.task}</p>`,
286
+ item.hint === undefined
287
+ ? ""
288
+ : `<details class="explainer-hint"><summary>${plain(message(lang, "hint"))}</summary><p>${item.hint}</p></details>`,
289
+ `<details class="explainer-solution"><summary>${plain(solution)}</summary>`,
290
+ item.steps.length === 0
291
+ ? ""
292
+ : `<ol class="explainer-steps">${item.steps.map((step) => `<li>${step}</li>`).join("")}</ol>`,
293
+ `<p><strong>${plain(questionMessagesIn(lang).rightAnswer)}</strong> ${item.answer}</p>`,
294
+ `</details>`,
295
+ `</li>`,
296
+ ]
297
+ .filter((part) => part !== "")
298
+ .join(""),
299
+ );
300
+ const exerciseData = generatedData(spec, lang, all, resolve);
301
+ return [
302
+ `<div class="explainer-exercise explainer-generated" id="${spec.id}">`,
303
+ `<p class="explainer-exercise-label">${plain(message(lang, "exerciseLabel"))}</p>`,
304
+ `<div class="explainer-live" hidden></div>`,
305
+ `<p class="explainer-worked-intro">${plain(message(lang, "workedTasks"))}</p>`,
306
+ `<ol class="explainer-worked">${worked.join("\n")}</ol>`,
307
+ `<script type="application/json" class="explainer-exercise-data">${json(exerciseData)}</script>`,
308
+ `</div>`,
309
+ ].join("\n");
310
+ }
311
+
312
+ /** An exercise of a page in `lang`; an open task keeps its answer while the tab is open under the app's `storage`. */
313
+ export function renderExercise(
314
+ block: Exercise,
315
+ lang: string,
316
+ data: Readonly<Record<string, readonly DataRecord[]>>,
317
+ resolve: (href: string) => string,
318
+ material: MaterialContext,
319
+ storage = "explainer",
320
+ ): string {
321
+ if (block.type === "generated") return generatedExercise(block, lang, data, resolve);
322
+ if (block.type === "open-task") return openTaskExercise(block, lang, resolve, storage);
323
+ return questionExercise(block, lang, material);
324
+ }