@lernapps/app-templates 0.0.1 → 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 +142 -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,540 @@
1
+ import { i as inLanguage, l as textsIn, s as localeOf, t as escape } from "./html-mffkHGHh.mjs";
2
+ import { i as materialIn, n as markerOf } from "./block-BK_ohsH1.mjs";
3
+ import { translator } from "@lernapps/tooling/i18n";
4
+ // runtime/shared/questions/messages/de.json
5
+ var de_default = {
6
+ check: "Prüfen",
7
+ next: "Nächste Frage",
8
+ right: "Richtig!",
9
+ wrong: "Leider nicht richtig.",
10
+ incomplete: "Antworte zuerst: Wähle, tippe oder ordne alles zu.",
11
+ tryAgain: "Versuche es noch einmal oder lass dir die Lösung zeigen.",
12
+ lookAgain: "Lies dazu noch einmal den Text über der Aufgabe.",
13
+ missingRight: "Es fehlt noch mindestens eine richtige Antwort.",
14
+ showSolution: "Lösung",
15
+ "true": "Stimmt",
16
+ "false": "Stimmt nicht",
17
+ yourAnswer: "Deine Antwort",
18
+ choose: "bitte wählen",
19
+ place: "Platz {place}",
20
+ orderHint: "Gib jedem Eintrag seinen Platz: 1 für den ersten.",
21
+ matchHint: "Wähle zu jedem Eintrag das Passende.",
22
+ solution: "Lösung",
23
+ rightAnswer: "Richtige Antwort:",
24
+ statementTrue: "Die Aussage stimmt.",
25
+ statementFalse: "Die Aussage stimmt nicht.",
26
+ tolerance: "(erlaubte Abweichung: {tolerance})",
27
+ rightOption: "richtige Antwort",
28
+ yourChoice: "deine Wahl",
29
+ moreReading: "Mehr dazu:"
30
+ };
31
+ // runtime/shared/questions/question.ts
32
+ /** The right entries a learner can choose from: those of the pairs, then the distractors. A chosen index counts into this list. */
33
+ const rightEntries = (question) => [...question.pairs.map((pair) => pair.right), ...question.distractors ?? []];
34
+ /**
35
+ * The pairs that `pairsFrom: "material"` takes from the first labelled figure of the material: left the marker of a
36
+ * label, right its text. A marker is the same in every language. Without `pairsFrom` there are none to take.
37
+ */
38
+ function materialPairs(question) {
39
+ if (question.pairsFrom === void 0) return void 0;
40
+ return ((question.material?.find((block) => block.type === "labelled-figure"))?.labels ?? []).map((label, index) => {
41
+ const marker = markerOf(label, index);
42
+ return {
43
+ left: typeof label.text === "string" ? marker : Object.fromEntries(Object.keys(label.text).map((lang) => [lang, marker])),
44
+ right: label.text
45
+ };
46
+ });
47
+ }
48
+ /** The question types. */
49
+ const QUESTION_TYPES = [
50
+ "single-choice",
51
+ "multiple-choice",
52
+ "true-false",
53
+ "number",
54
+ "ordering",
55
+ "matching"
56
+ ];
57
+ /** Each learner text of a question with its place in it: ["options", 1, "info"]. */
58
+ function questionTexts(question) {
59
+ const texts = [
60
+ {
61
+ path: ["text"],
62
+ text: question.text
63
+ },
64
+ {
65
+ path: ["explanation"],
66
+ text: question.explanation
67
+ },
68
+ ...question.links.map((link, index) => ({
69
+ path: [
70
+ "links",
71
+ index,
72
+ "title"
73
+ ],
74
+ text: link.title
75
+ })),
76
+ ...question.hint === void 0 ? [] : [{
77
+ path: ["hint"],
78
+ text: question.hint
79
+ }]
80
+ ];
81
+ switch (question.type) {
82
+ case "single-choice":
83
+ case "multiple-choice":
84
+ question.options.forEach((option, index) => {
85
+ texts.push({
86
+ path: [
87
+ "options",
88
+ index,
89
+ "text"
90
+ ],
91
+ text: option.text
92
+ });
93
+ texts.push({
94
+ path: [
95
+ "options",
96
+ index,
97
+ "info"
98
+ ],
99
+ text: option.info
100
+ });
101
+ if (option.link) texts.push({
102
+ path: [
103
+ "options",
104
+ index,
105
+ "link",
106
+ "title"
107
+ ],
108
+ text: option.link.title
109
+ });
110
+ });
111
+ break;
112
+ case "number":
113
+ if (question.unit !== void 0) texts.push({
114
+ path: ["unit"],
115
+ text: question.unit
116
+ });
117
+ question.mistakes?.forEach((mistake, index) => texts.push({
118
+ path: [
119
+ "mistakes",
120
+ index,
121
+ "message"
122
+ ],
123
+ text: mistake.message
124
+ }));
125
+ break;
126
+ case "ordering":
127
+ question.items.forEach((item, index) => texts.push({
128
+ path: ["items", index],
129
+ text: item
130
+ }));
131
+ break;
132
+ case "matching":
133
+ (question.pairsFrom === void 0 ? question.pairs : []).forEach((pair, index) => {
134
+ texts.push({
135
+ path: [
136
+ "pairs",
137
+ index,
138
+ "left"
139
+ ],
140
+ text: pair.left
141
+ });
142
+ texts.push({
143
+ path: [
144
+ "pairs",
145
+ index,
146
+ "right"
147
+ ],
148
+ text: pair.right
149
+ });
150
+ });
151
+ question.distractors?.forEach((text, index) => texts.push({
152
+ path: ["distractors", index],
153
+ text
154
+ }));
155
+ }
156
+ return texts;
157
+ }
158
+ /** Each link's address of a question with its place in it: ["links", 0, "url"]. */
159
+ function questionAddresses(question) {
160
+ const options = question.type === "single-choice" || question.type === "multiple-choice" ? question.options : [];
161
+ return [...question.links.map((link, index) => ({
162
+ path: [
163
+ "links",
164
+ index,
165
+ "url"
166
+ ],
167
+ text: link.url
168
+ })), ...options.flatMap((option, index) => option.link === void 0 ? [] : [{
169
+ path: [
170
+ "options",
171
+ index,
172
+ "link",
173
+ "url"
174
+ ],
175
+ text: option.link.url
176
+ }])];
177
+ }
178
+ /** The question in `lang`: each text and address from its map, a plain string as it is; without its examples. */
179
+ function questionIn(question, lang) {
180
+ const t = (text) => inLanguage(text, lang);
181
+ const link = (value) => ({
182
+ title: t(value.title),
183
+ url: t(value.url)
184
+ });
185
+ const base = {
186
+ id: question.id,
187
+ text: t(question.text),
188
+ explanation: t(question.explanation),
189
+ links: question.links.map(link),
190
+ ...question.hint === void 0 ? {} : { hint: t(question.hint) },
191
+ ...question.material === void 0 ? {} : { material: materialIn(question.material, lang) }
192
+ };
193
+ switch (question.type) {
194
+ case "single-choice":
195
+ case "multiple-choice": return {
196
+ ...base,
197
+ type: question.type,
198
+ options: question.options.map((option) => ({
199
+ text: t(option.text),
200
+ correct: option.correct,
201
+ info: t(option.info),
202
+ ...option.link === void 0 ? {} : { link: link(option.link) }
203
+ }))
204
+ };
205
+ case "true-false": return {
206
+ ...base,
207
+ type: question.type,
208
+ answer: question.answer
209
+ };
210
+ case "number": return {
211
+ ...base,
212
+ type: question.type,
213
+ answer: question.answer,
214
+ tolerance: question.tolerance,
215
+ ...question.unit === void 0 ? {} : { unit: t(question.unit) },
216
+ ...question.mistakes === void 0 ? {} : { mistakes: question.mistakes.map((mistake) => ({
217
+ answer: mistake.answer,
218
+ message: t(mistake.message)
219
+ })) }
220
+ };
221
+ case "ordering": return {
222
+ ...base,
223
+ type: question.type,
224
+ items: question.items.map(t)
225
+ };
226
+ case "matching": return {
227
+ ...base,
228
+ type: question.type,
229
+ pairs: (materialPairs(question) ?? question.pairs).map((pair) => ({
230
+ left: t(pair.left),
231
+ right: t(pair.right)
232
+ })),
233
+ ...question.distractors === void 0 ? {} : { distractors: question.distractors.map(t) }
234
+ };
235
+ }
236
+ }
237
+ /** The id of a question's element on the page, and its address (#frage-<id>). */
238
+ const questionId = (question) => `frage-${question.id}`;
239
+ /** A number as a learner types it: comma or point as decimal sign, spaces, the unit after it. */
240
+ function parseNumber(input, unit) {
241
+ let text = input.trim();
242
+ if (unit !== void 0 && text.endsWith(unit)) text = text.slice(0, -unit.length);
243
+ text = text.replace(/[\s ]/g, "").replace(",", ".");
244
+ if (!/^[+\-−]?(\d+\.?\d*|\.\d+)$/.test(text)) return void 0;
245
+ return Number(text.replace("−", "-"));
246
+ }
247
+ const isIndexList = (answer) => Array.isArray(answer);
248
+ /** Whether `value` is within the tolerance of `target`, with a little slack for decimal rounding. */
249
+ const within = (value, target, tolerance) => Math.abs(value - target) <= tolerance + 1e-9;
250
+ /** Whether `answer` is the right answer to `question`; an answer of the wrong shape is not. */
251
+ function isCorrect(question, answer) {
252
+ switch (question.type) {
253
+ case "single-choice": return typeof answer === "number" && question.options[answer]?.correct === true;
254
+ case "multiple-choice": {
255
+ if (!isIndexList(answer)) return false;
256
+ const chosen = new Set(answer);
257
+ return question.options.every((option, index) => option.correct === chosen.has(index));
258
+ }
259
+ case "true-false": return answer === question.answer;
260
+ case "number": {
261
+ if (typeof answer !== "string") return false;
262
+ const value = parseNumber(answer, question.unit);
263
+ return value !== void 0 && within(value, question.answer, question.tolerance);
264
+ }
265
+ case "ordering": return isIndexList(answer) && answer.length === question.items.length && answer.every((place, i) => place === i);
266
+ case "matching": return isIndexList(answer) && answer.length === question.pairs.length && answer.every((pair, i) => pair === i);
267
+ }
268
+ }
269
+ /**
270
+ * Why `answer` to `question` is wrong, as specifically as the question says it: for a choice the background of each
271
+ * wrong option the learner chose (with the option), for a number the message of the typical mistake it matches; else
272
+ * the question's hint. None for a right answer, nor when the question says nothing more.
273
+ */
274
+ function wrongReasons(question, answer) {
275
+ if (isCorrect(question, answer)) return [];
276
+ const specific = [];
277
+ if (question.type === "single-choice" || question.type === "multiple-choice") {
278
+ const chosen = typeof answer === "number" ? [answer] : isIndexList(answer) ? answer : [];
279
+ for (const index of chosen) {
280
+ const option = question.options[index];
281
+ if (option !== void 0 && !option.correct && option.info !== "") specific.push({
282
+ option: option.text,
283
+ text: option.info
284
+ });
285
+ }
286
+ }
287
+ if (question.type === "number" && typeof answer === "string") {
288
+ const value = parseNumber(answer, question.unit);
289
+ const mistake = value === void 0 ? void 0 : question.mistakes?.find((candidate) => within(value, candidate.answer, question.tolerance));
290
+ if (mistake !== void 0) specific.push({ text: mistake.message });
291
+ }
292
+ if (specific.length > 0) return specific;
293
+ return question.hint === void 0 ? [] : [{ text: question.hint }];
294
+ }
295
+ /** Whether a multiple-choice answer chose only right options, but not all of them. */
296
+ const missesRight = (question, answer) => question.type === "multiple-choice" && isIndexList(answer) && answer.every((index) => question.options[index]?.correct === true) && !isCorrect(question, answer);
297
+ /** Whether the learner has answered at all: something chosen, typed or placed everywhere. */
298
+ function isComplete(question, answer) {
299
+ if (answer === void 0) return false;
300
+ switch (question.type) {
301
+ case "single-choice": return typeof answer === "number";
302
+ case "multiple-choice": return isIndexList(answer) && answer.length > 0;
303
+ case "true-false": return typeof answer === "boolean";
304
+ case "number": return typeof answer === "string" && parseNumber(answer, question.unit) !== void 0;
305
+ case "ordering":
306
+ case "matching": return isIndexList(answer) && answer.every((value) => Number.isInteger(value) && value >= 0);
307
+ }
308
+ }
309
+ /** The right answer, as a learner would give it. */
310
+ function rightAnswer$1(question) {
311
+ switch (question.type) {
312
+ case "single-choice": return question.options.findIndex((option) => option.correct);
313
+ case "multiple-choice": return question.options.flatMap((option, index) => option.correct ? [index] : []);
314
+ case "true-false": return question.answer;
315
+ case "number": return String(question.answer).replace(".", ",");
316
+ case "ordering": return question.items.map((_item, index) => index);
317
+ case "matching": return question.pairs.map((_pair, index) => index);
318
+ }
319
+ }
320
+ /** A complete answer that is wrong, as a learner might give it. */
321
+ function wrongAnswer(question) {
322
+ switch (question.type) {
323
+ case "single-choice": return question.options.findIndex((option) => !option.correct);
324
+ case "multiple-choice": {
325
+ const right = question.options.flatMap((option, index) => option.correct ? [index] : []);
326
+ const wrong = question.options.findIndex((option) => !option.correct);
327
+ return wrong >= 0 ? [...right, wrong].sort((a, b) => a - b) : right.slice(1);
328
+ }
329
+ case "true-false": return !question.answer;
330
+ case "number": return String(question.answer + question.tolerance + Math.max(1, Math.abs(question.answer))).replace(".", ",");
331
+ case "ordering": return question.items.map((_item, index) => question.items.length - 1 - index);
332
+ case "matching": return question.pairs.map((_pair, index) => (index + 1) % question.pairs.length);
333
+ }
334
+ }
335
+ /** A small seeded random generator (mulberry32): the same seed gives the same order everywhere. */
336
+ function random(seed) {
337
+ let state = seed >>> 0;
338
+ return () => {
339
+ state = state + 1831565813 >>> 0;
340
+ let t = state;
341
+ t = Math.imul(t ^ t >>> 15, t | 1);
342
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
343
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
344
+ };
345
+ }
346
+ /** A number from a text, to derive a seed per question. */
347
+ function hash(text) {
348
+ let value = 2166136261;
349
+ for (let i = 0; i < text.length; i++) value = Math.imul(value ^ text.charCodeAt(i), 16777619);
350
+ return value >>> 0;
351
+ }
352
+ /** The indices 0..length-1 in an order given by `seed`. */
353
+ function shuffled(length, seed) {
354
+ const next = random(seed);
355
+ const order = Array.from({ length }, (_value, index) => index);
356
+ for (let i = order.length - 1; i > 0; i--) {
357
+ const j = Math.floor(next() * (i + 1));
358
+ [order[i], order[j]] = [order[j] ?? j, order[i] ?? i];
359
+ }
360
+ return order;
361
+ }
362
+ /**
363
+ * The order in which the page shows the items of an ordering or the right entries of a matching question: mixed,
364
+ * the same on every build, and never the right order itself.
365
+ */
366
+ function mixed(length, id) {
367
+ for (let attempt = 0;; attempt++) {
368
+ const order = shuffled(length, hash(`${id}#${attempt}`));
369
+ if (length < 2 || order.some((value, index) => value !== index)) return order;
370
+ }
371
+ }
372
+ // runtime/shared/questions/render.ts
373
+ /** The texts of a question for learners in each language it has them: an app may declare only these languages. */
374
+ const QUESTION_MESSAGES = {
375
+ de: de_default,
376
+ en: {
377
+ check: "Check",
378
+ next: "Next question",
379
+ right: "Right!",
380
+ wrong: "Sorry, that is not right.",
381
+ incomplete: "Answer first: choose, type or match everything.",
382
+ tryAgain: "Try again, or show the solution.",
383
+ lookAgain: "Read the text above the exercise again.",
384
+ missingRight: "At least one right answer is still missing.",
385
+ showSolution: "Solution",
386
+ "true": "True",
387
+ "false": "False",
388
+ yourAnswer: "Your answer",
389
+ choose: "please choose",
390
+ place: "Place {place}",
391
+ orderHint: "Give each entry its place: 1 for the first.",
392
+ matchHint: "Choose the match for each entry.",
393
+ solution: "Solution",
394
+ rightAnswer: "Right answer:",
395
+ statementTrue: "The statement is true.",
396
+ statementFalse: "The statement is false.",
397
+ tolerance: "(allowed difference: {tolerance})",
398
+ rightOption: "right answer",
399
+ yourChoice: "your choice",
400
+ moreReading: "More on it:"
401
+ }
402
+ };
403
+ /** The question's texts in `lang`; German for a language it does not have. */
404
+ const questionMessagesIn = (lang) => textsIn(QUESTION_MESSAGES, lang);
405
+ const languages = /* @__PURE__ */ new Map();
406
+ function questionWords(lang = "de") {
407
+ let found = languages.get(lang);
408
+ if (found === void 0) {
409
+ const locale = localeOf(lang);
410
+ found = {
411
+ t: translator(questionMessagesIn(lang), locale),
412
+ numbers: new Intl.NumberFormat(locale, { maximumFractionDigits: 10 })
413
+ };
414
+ languages.set(lang, found);
415
+ }
416
+ return found;
417
+ }
418
+ function controls(question, { t }, format) {
419
+ const name = questionId(question);
420
+ switch (question.type) {
421
+ case "single-choice":
422
+ case "multiple-choice": {
423
+ const type = question.type === "single-choice" ? "radio" : "checkbox";
424
+ return question.options.map((option, index) => `<label class="question-option"><input type="${type}" name="${name}" value="${index}"> <span>${format(option.text)}</span></label>`).join("\n");
425
+ }
426
+ case "true-false": return [true, false].map((value) => `<label class="question-option"><input type="radio" name="${name}" value="${value}"> <span>${t(value ? "true" : "false")}</span></label>`).join("\n");
427
+ case "number": return [
428
+ `<label class="question-number">${t("yourAnswer")}`,
429
+ `<input type="text" inputmode="decimal" autocomplete="off" name="${name}"${question.unit === void 0 ? "" : ` aria-describedby="${name}-unit"`}>`,
430
+ question.unit === void 0 ? "" : `<span id="${name}-unit">${format(question.unit)}</span>`,
431
+ `</label>`
432
+ ].join(" ");
433
+ case "ordering": {
434
+ const places = question.items.map((_item, place) => `<option value="${place}">${t("place", { place: place + 1 })}</option>`).join("");
435
+ return [
436
+ `<p class="question-hint">${t("orderHint")}</p>`,
437
+ `<ul class="question-pairs">`,
438
+ ...mixed(question.items.length, question.id).map((index) => `<li><label><span>${format(question.items[index] ?? "")}</span> <select name="${name}-${index}" data-item="${index}"><option value="">${t("choose")}</option>${places}</select></label></li>`),
439
+ `</ul>`
440
+ ].join("\n");
441
+ }
442
+ case "matching": {
443
+ const entries = rightEntries(question);
444
+ const rights = mixed(entries.length, question.id).map((index) => `<option value="${index}">${escape(entries[index] ?? "")}</option>`).join("");
445
+ return [
446
+ `<p class="question-hint">${t("matchHint")}</p>`,
447
+ `<ul class="question-pairs">`,
448
+ ...question.pairs.map((pair, index) => `<li><label><span>${format(pair.left)}</span> <select name="${name}-${index}" data-left="${index}"><option value="">${t("choose")}</option>${rights}</select></label></li>`),
449
+ `</ul>`
450
+ ].join("\n");
451
+ }
452
+ }
453
+ }
454
+ /**
455
+ * One question with its controls, the button to check it, the place of its feedback and, hidden until it is answered,
456
+ * what the learner learns about it (the same as its solution). The engine's startQuestion makes it live; without
457
+ * JavaScript the button stays hidden.
458
+ */
459
+ function renderQuestion(question, { element = "div", next = false, lang = "de", textHtml, materialHtml, explanationHtml, format = escape } = {}) {
460
+ const words = questionWords(lang);
461
+ const { t } = words;
462
+ const id = questionId(question);
463
+ const nextButton = next ? ` <button type="button" class="question-next" hidden>${t("next")}</button>` : "";
464
+ return [
465
+ `<${element} class="question" id="${id}" data-id="${question.id}" data-type="${question.type}">`,
466
+ `<form class="question-form" novalidate>`,
467
+ `<fieldset>`,
468
+ `<legend class="question-text">${textHtml ?? format(question.text)}</legend>`,
469
+ ...materialHtml === void 0 || materialHtml === "" ? [] : [`<div class="question-material">${materialHtml}</div>`],
470
+ controls(question, words, format),
471
+ `</fieldset>`,
472
+ `<div class="question-feedback" role="status"></div>`,
473
+ `<div class="question-reveal" hidden>`,
474
+ renderSolution(question, {
475
+ lang,
476
+ format,
477
+ ...explanationHtml === void 0 ? {} : { explanationHtml }
478
+ }),
479
+ `</div>`,
480
+ `<p class="question-actions" hidden><button type="submit" class="question-check">${t("check")}</button>${nextButton}</p>`,
481
+ `</form>`,
482
+ `</${element}>`
483
+ ].join("\n");
484
+ }
485
+ /** The right answer as the solution states it, in `lang`: the right options, the number, the order, the pairs. */
486
+ function answerText(question, lang = "de", format = escape) {
487
+ return rightAnswerText(question, questionWords(lang), format);
488
+ }
489
+ function rightAnswerText(question, { t, numbers }, format) {
490
+ switch (question.type) {
491
+ case "single-choice":
492
+ case "multiple-choice": return question.options.filter((option) => option.correct).map((option) => format(option.text)).join("; ");
493
+ case "true-false": return t(question.answer ? "statementTrue" : "statementFalse");
494
+ case "number": {
495
+ const unit = question.unit === void 0 ? "" : ` ${format(question.unit)}`;
496
+ const tolerance = question.tolerance > 0 ? ` ${t("tolerance", { tolerance: `${numbers.format(question.tolerance)}${unit}` })}` : "";
497
+ return `${numbers.format(question.answer)}${unit}${tolerance}`;
498
+ }
499
+ case "ordering": return question.items.map((item, index) => `${index + 1}. ${format(item)}`).join(", ");
500
+ case "matching": return question.pairs.map((pair) => `${format(pair.left)} – ${format(pair.right)}`).join("; ");
501
+ }
502
+ }
503
+ /** A link to another site: opened like any link, never loaded before a click, without access to this page. */
504
+ const external = (link) => `<a href="${escape(link.url)}" rel="noopener">${escape(link.title)}</a>`;
505
+ /** Every option with its background, the right ones marked; the engine marks the learner's choice. */
506
+ function optionInfo(question, words, format) {
507
+ const { t } = words;
508
+ if (question.type !== "single-choice" && question.type !== "multiple-choice") return `<p><strong>${t("rightAnswer")}</strong> ${rightAnswerText(question, words, format)}</p>`;
509
+ return [
510
+ `<ul class="question-infos">`,
511
+ ...question.options.map((option, index) => [
512
+ `<li class="question-info${option.correct ? " question-info-right" : ""}" data-option="${index}">`,
513
+ `<p class="question-info-head"><strong>${format(option.text)}</strong>${option.correct ? ` <span class="question-badge question-badge-right">${t("rightOption")}</span>` : ""}</p>`,
514
+ option.info === "" ? "" : `<p>${format(option.info)}</p>`,
515
+ option.link === void 0 ? "" : `<p>${external(option.link)}</p>`,
516
+ `</li>`
517
+ ].join("")),
518
+ `</ul>`
519
+ ].join("\n");
520
+ }
521
+ /**
522
+ * What the learner learns about a question, its solution: the right answer (the right options marked), each option's
523
+ * background, the explanation, further reading. A fragment without ids, e.g. for a <details>.
524
+ */
525
+ function renderSolution(question, { lang = "de", explanationHtml, format = escape } = {}) {
526
+ const words = questionWords(lang);
527
+ const links = question.links.length === 0 ? [] : [
528
+ `<p class="question-more">${words.t("moreReading")}</p>`,
529
+ `<ul class="question-links">`,
530
+ ...question.links.map((link) => `<li>${external(link)}</li>`),
531
+ `</ul>`
532
+ ];
533
+ const explanation = explanationHtml ?? (question.explanation === "" ? "" : `<p class="question-explanation">${format(question.explanation)}</p>`);
534
+ return [
535
+ optionInfo(question, words, format),
536
+ explanation,
537
+ ...links
538
+ ].filter((part) => part !== "").join("\n");
539
+ }
540
+ export { wrongAnswer as C, shuffled as S, de_default as T, questionIn as _, renderQuestion as a, rightAnswer$1 as b, hash as c, materialPairs as d, missesRight as f, questionId as g, questionAddresses as h, questionMessagesIn as i, isComplete as l, parseNumber as m, answerText as n, renderSolution as o, mixed as p, external as r, QUESTION_TYPES as s, QUESTION_MESSAGES as t, isCorrect as u, questionTexts as v, wrongReasons as w, rightEntries as x, random as y };