@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,1696 @@
1
+ import { i as inLanguage, l as textsIn, s as localeOf, t as escape } from "./html-mffkHGHh.mjs";
2
+ import { S as shuffled, c as hash, m as parseNumber, u as isCorrect, y as random } from "./render-BJey0bwl.mjs";
3
+ import { createStorage } from "@lernapps/tooling/storage";
4
+ // runtime/explainer/fraction.ts
5
+ const abs = (n) => n < 0n ? -n : n;
6
+ function gcd(a, b) {
7
+ let x = abs(a);
8
+ let y = abs(b);
9
+ while (y !== 0n) [x, y] = [y, x % y];
10
+ return x;
11
+ }
12
+ var Fraction = class Fraction {
13
+ n;
14
+ d;
15
+ constructor(n, d) {
16
+ if (d === 0n) throw new RangeError("division by zero");
17
+ const sign = d < 0n ? -1n : 1n;
18
+ const g = gcd(n, d) || 1n;
19
+ this.n = sign * n / g;
20
+ this.d = sign * d / g;
21
+ }
22
+ static of(n, d = 1) {
23
+ return new Fraction(BigInt(n), BigInt(d));
24
+ }
25
+ /** A number as written: 0.1 is exactly 1/10. */
26
+ static from(value) {
27
+ if (!Number.isFinite(value)) throw new RangeError(`${value} is not a number`);
28
+ const text = String(value);
29
+ const match = /^(-?)(\d+)(?:\.(\d+))?(?:e([+-]\d+))?$/.exec(text);
30
+ if (match === null) throw new RangeError(`${text} is not a number`);
31
+ const [, sign = "", whole = "0", part = "", exponent = "0"] = match;
32
+ let n = BigInt(`${sign}${whole}${part}`);
33
+ let d = 10n ** BigInt(part.length);
34
+ const e = Number(exponent);
35
+ if (e > 0) n *= 10n ** BigInt(e);
36
+ else if (e < 0) d *= 10n ** BigInt(-e);
37
+ return new Fraction(n, d);
38
+ }
39
+ add(other) {
40
+ return new Fraction(this.n * other.d + other.n * this.d, this.d * other.d);
41
+ }
42
+ sub(other) {
43
+ return new Fraction(this.n * other.d - other.n * this.d, this.d * other.d);
44
+ }
45
+ mul(other) {
46
+ return new Fraction(this.n * other.n, this.d * other.d);
47
+ }
48
+ div(other) {
49
+ if (other.n === 0n) throw new RangeError("division by zero");
50
+ return new Fraction(this.n * other.d, this.d * other.n);
51
+ }
52
+ neg() {
53
+ return new Fraction(-this.n, this.d);
54
+ }
55
+ abs() {
56
+ return new Fraction(abs(this.n), this.d);
57
+ }
58
+ compare(other) {
59
+ const left = this.n * other.d;
60
+ const right = other.n * this.d;
61
+ return left < right ? -1 : left > right ? 1 : 0;
62
+ }
63
+ equals(other) {
64
+ return this.n === other.n && this.d === other.d;
65
+ }
66
+ isInteger() {
67
+ return this.d === 1n;
68
+ }
69
+ /** Rounded to `places` decimals, half away from zero. */
70
+ round(places) {
71
+ const scale = 10n ** BigInt(Math.max(0, places));
72
+ const scaled = abs(this.n) * scale;
73
+ const q = scaled / this.d;
74
+ const rounded = 2n * (scaled % this.d) >= this.d ? q + 1n : q;
75
+ return new Fraction(this.n < 0n ? -rounded : rounded, scale);
76
+ }
77
+ /** The number of decimals when written out; Infinity when they never end (1/3). */
78
+ decimals() {
79
+ let d = this.d;
80
+ let twos = 0;
81
+ let fives = 0;
82
+ while (d % 2n === 0n) {
83
+ d /= 2n;
84
+ twos++;
85
+ }
86
+ while (d % 5n === 0n) {
87
+ d /= 5n;
88
+ fives++;
89
+ }
90
+ return d === 1n ? Math.max(twos, fives) : Infinity;
91
+ }
92
+ toNumber() {
93
+ return Number(this.n) / Number(this.d);
94
+ }
95
+ toString() {
96
+ return this.d === 1n ? String(this.n) : `${this.n}/${this.d}`;
97
+ }
98
+ };
99
+ // runtime/explainer/expression.ts
100
+ /** A text in several languages, from a data file or a list: compared and sorted by its German text. */
101
+ var Localized = class {
102
+ texts;
103
+ constructor(texts) {
104
+ this.texts = texts;
105
+ }
106
+ in(lang) {
107
+ return this.texts[lang] ?? this.texts["de"] ?? "";
108
+ }
109
+ toString() {
110
+ return this.in("de");
111
+ }
112
+ };
113
+ var ExpressionError = class extends Error {};
114
+ /** The functions, with their number of arguments and which arguments are field names. */
115
+ const FUNCTIONS = {
116
+ round: { arity: 2 },
117
+ decimals: { arity: 1 },
118
+ map: {
119
+ arity: 2,
120
+ field: 1
121
+ },
122
+ sortBy: {
123
+ arity: 2,
124
+ field: 1
125
+ },
126
+ reverse: { arity: 1 }
127
+ };
128
+ const KEYWORDS = /* @__PURE__ */ new Set([
129
+ "and",
130
+ "or",
131
+ "not",
132
+ "true",
133
+ "false"
134
+ ]);
135
+ function tokens(source) {
136
+ const list = [];
137
+ const pattern = /\s*(?:(\d+(?:\.\d+)?)|"([^"]*)"|'([^']*)'|([A-Za-z_][A-Za-z0-9_]*)|(==|!=|<=|>=|[-+*/()<>[\],.]|=)|(\S))/y;
138
+ let match;
139
+ pattern.lastIndex = 0;
140
+ while (pattern.lastIndex < source.length && (match = pattern.exec(source)) !== null) {
141
+ const at = match.index + match[0].length - match[0].trimStart().length;
142
+ if (match[0].trim() === "") break;
143
+ if (match[1] !== void 0) list.push({
144
+ type: "number",
145
+ text: match[1],
146
+ at
147
+ });
148
+ else if (match[2] !== void 0 || match[3] !== void 0) list.push({
149
+ type: "string",
150
+ text: match[2] ?? match[3] ?? "",
151
+ at
152
+ });
153
+ else if (match[4] !== void 0) list.push({
154
+ type: "name",
155
+ text: match[4],
156
+ at
157
+ });
158
+ else if (match[5] !== void 0) {
159
+ if (match[5] === "=") throw new ExpressionError(`= at ${at + 1}: use == to compare`);
160
+ list.push({
161
+ type: "op",
162
+ text: match[5],
163
+ at
164
+ });
165
+ } else throw new ExpressionError(`unexpected ${match[6] ?? "character"} at ${at + 1}`);
166
+ }
167
+ list.push({
168
+ type: "end",
169
+ text: "the end",
170
+ at: source.length
171
+ });
172
+ return list;
173
+ }
174
+ /** Parses `source` into an expression; throws an ExpressionError that says where it does not parse. */
175
+ function parse(source) {
176
+ const list = tokens(source);
177
+ let position = 0;
178
+ const peek = () => list[position] ?? {
179
+ type: "end",
180
+ text: "the end",
181
+ at: source.length
182
+ };
183
+ const next = () => {
184
+ const token = peek();
185
+ position++;
186
+ return token;
187
+ };
188
+ const isOp = (text) => peek().type === "op" && peek().text === text;
189
+ const isWord = (text) => peek().type === "name" && peek().text === text;
190
+ const expect = (text) => {
191
+ const token = next();
192
+ if (token.text !== text) throw new ExpressionError(`expected ${text} at ${token.at + 1}, found ${token.text}`);
193
+ };
194
+ const or = () => {
195
+ let left = and();
196
+ while (isWord("or")) {
197
+ next();
198
+ left = {
199
+ kind: "binary",
200
+ op: "or",
201
+ left,
202
+ right: and()
203
+ };
204
+ }
205
+ return left;
206
+ };
207
+ const and = () => {
208
+ let left = not();
209
+ while (isWord("and")) {
210
+ next();
211
+ left = {
212
+ kind: "binary",
213
+ op: "and",
214
+ left,
215
+ right: not()
216
+ };
217
+ }
218
+ return left;
219
+ };
220
+ const not = () => {
221
+ if (isWord("not")) {
222
+ next();
223
+ return {
224
+ kind: "unary",
225
+ op: "not",
226
+ arg: not()
227
+ };
228
+ }
229
+ return comparison();
230
+ };
231
+ const comparison = () => {
232
+ const left = sum();
233
+ const token = peek();
234
+ if (token.type === "op" && [
235
+ "==",
236
+ "!=",
237
+ "<",
238
+ "<=",
239
+ ">",
240
+ ">="
241
+ ].includes(token.text)) {
242
+ next();
243
+ return {
244
+ kind: "binary",
245
+ op: token.text,
246
+ left,
247
+ right: sum()
248
+ };
249
+ }
250
+ return left;
251
+ };
252
+ const sum = () => {
253
+ let left = product();
254
+ while (isOp("+") || isOp("-")) left = {
255
+ kind: "binary",
256
+ op: next().text,
257
+ left,
258
+ right: product()
259
+ };
260
+ return left;
261
+ };
262
+ const product = () => {
263
+ let left = unary();
264
+ while (isOp("*") || isOp("/")) left = {
265
+ kind: "binary",
266
+ op: next().text,
267
+ left,
268
+ right: unary()
269
+ };
270
+ return left;
271
+ };
272
+ const unary = () => {
273
+ if (isOp("-")) {
274
+ next();
275
+ return {
276
+ kind: "unary",
277
+ op: "-",
278
+ arg: unary()
279
+ };
280
+ }
281
+ return postfix();
282
+ };
283
+ const postfix = () => {
284
+ let node = primary();
285
+ for (;;) if (isOp(".")) {
286
+ next();
287
+ const field = next();
288
+ if (field.type !== "name") throw new ExpressionError(`expected a field name at ${field.at + 1}`);
289
+ node = {
290
+ kind: "field",
291
+ of: node,
292
+ field: field.text
293
+ };
294
+ } else if (isOp("[")) {
295
+ next();
296
+ const index = or();
297
+ expect("]");
298
+ node = {
299
+ kind: "index",
300
+ of: node,
301
+ index
302
+ };
303
+ } else return node;
304
+ };
305
+ const primary = () => {
306
+ const token = next();
307
+ if (token.type === "number") return {
308
+ kind: "number",
309
+ value: Fraction.from(Number(token.text))
310
+ };
311
+ if (token.type === "string") return {
312
+ kind: "string",
313
+ value: token.text
314
+ };
315
+ if (token.type === "name") {
316
+ if (token.text === "true" || token.text === "false") return {
317
+ kind: "boolean",
318
+ value: token.text === "true"
319
+ };
320
+ if (KEYWORDS.has(token.text)) throw new ExpressionError(`unexpected ${token.text} at ${token.at + 1}`);
321
+ if (!isOp("(")) return {
322
+ kind: "name",
323
+ name: token.text
324
+ };
325
+ const fn = FUNCTIONS[token.text];
326
+ if (fn === void 0 || !Object.hasOwn(FUNCTIONS, token.text)) throw new ExpressionError(`${token.text} is not a function of the expression language; use one of ${Object.keys(FUNCTIONS).join(", ")}`);
327
+ next();
328
+ const args = [];
329
+ if (!isOp(")")) {
330
+ args.push(or());
331
+ while (isOp(",")) {
332
+ next();
333
+ args.push(or());
334
+ }
335
+ }
336
+ expect(")");
337
+ if (args.length !== fn.arity) throw new ExpressionError(`${token.text} takes ${fn.arity} argument${fn.arity === 1 ? "" : "s"}`);
338
+ if (fn.field !== void 0 && args[fn.field]?.kind !== "name") throw new ExpressionError(`the argument ${fn.field + 1} of ${token.text} is a field name, e.g. year`);
339
+ return {
340
+ kind: "call",
341
+ fn: token.text,
342
+ args
343
+ };
344
+ }
345
+ if (token.type === "op" && token.text === "(") {
346
+ const inner = or();
347
+ expect(")");
348
+ return inner;
349
+ }
350
+ if (token.type === "op" && token.text === "[") {
351
+ const items = [];
352
+ if (!isOp("]")) {
353
+ items.push(or());
354
+ while (isOp(",")) {
355
+ next();
356
+ items.push(or());
357
+ }
358
+ }
359
+ expect("]");
360
+ return {
361
+ kind: "list",
362
+ items
363
+ };
364
+ }
365
+ throw new ExpressionError(`unexpected ${token.text} at ${token.at + 1}`);
366
+ };
367
+ const tree = or();
368
+ const rest = peek();
369
+ if (rest.type !== "end") throw new ExpressionError(`unexpected ${rest.text} at ${rest.at + 1}`);
370
+ return tree;
371
+ }
372
+ /** The variables an expression uses: its names, not its functions or the field names they take. */
373
+ function variables(node, into = /* @__PURE__ */ new Set()) {
374
+ switch (node.kind) {
375
+ case "name":
376
+ into.add(node.name);
377
+ break;
378
+ case "list":
379
+ for (const item of node.items) variables(item, into);
380
+ break;
381
+ case "field":
382
+ variables(node.of, into);
383
+ break;
384
+ case "index":
385
+ variables(node.of, into);
386
+ variables(node.index, into);
387
+ break;
388
+ case "call":
389
+ node.args.forEach((arg, index) => {
390
+ if (FUNCTIONS[node.fn]?.field !== index) variables(arg, into);
391
+ });
392
+ break;
393
+ case "unary":
394
+ variables(node.arg, into);
395
+ break;
396
+ case "binary":
397
+ variables(node.left, into);
398
+ variables(node.right, into);
399
+ }
400
+ return into;
401
+ }
402
+ const isList = (value) => Array.isArray(value);
403
+ const isRecord = (value) => typeof value === "object" && !Array.isArray(value) && !(value instanceof Fraction) && !(value instanceof Localized);
404
+ /** Whether two values are the same: numbers by value, texts by their German text, lists and records item by item. */
405
+ function same(a, b) {
406
+ if (a instanceof Fraction || b instanceof Fraction) return a instanceof Fraction && b instanceof Fraction && a.equals(b);
407
+ if (isList(a) || isList(b)) return isList(a) && isList(b) && a.length === b.length && a.every((item, index) => same(item, b[index]));
408
+ if (typeof a === "boolean" || typeof b === "boolean") return a === b;
409
+ if (isRecord(a) || isRecord(b)) {
410
+ if (!isRecord(a) || !isRecord(b)) return false;
411
+ const keys = Object.keys(a);
412
+ return keys.length === Object.keys(b).length && keys.every((key) => key in b && same(a[key], b[key]));
413
+ }
414
+ return String(a) === String(b);
415
+ }
416
+ /** The order of two values: numbers by value, texts by their German text. */
417
+ function order(a, b, what) {
418
+ if (a instanceof Fraction && b instanceof Fraction) return a.compare(b);
419
+ const text = (value) => typeof value === "string" || value instanceof Localized ? String(value) : void 0;
420
+ const left = text(a);
421
+ const right = text(b);
422
+ if (left === void 0 || right === void 0) throw new ExpressionError(`${what} compares numbers or texts`);
423
+ return left < right ? -1 : left > right ? 1 : 0;
424
+ }
425
+ const describe = (value) => value instanceof Fraction ? "a number" : typeof value === "string" || value instanceof Localized ? "a text" : typeof value === "boolean" ? "true or false" : isList(value) ? "a list" : "a record";
426
+ function asNumber(value, what) {
427
+ if (!(value instanceof Fraction)) throw new ExpressionError(`${what} needs numbers, not ${describe(value)}`);
428
+ return value;
429
+ }
430
+ function asList(value, what) {
431
+ if (!isList(value)) throw new ExpressionError(`${what} needs a list, not ${describe(value)}`);
432
+ return value;
433
+ }
434
+ function asBoolean(value, what) {
435
+ if (typeof value !== "boolean") throw new ExpressionError(`${what} needs true or false, not ${describe(value)}`);
436
+ return value;
437
+ }
438
+ function fieldOf(value, field) {
439
+ if (!isRecord(value)) throw new ExpressionError(`.${field} needs a record, not ${describe(value)}`);
440
+ if (!Object.hasOwn(value, field)) throw new ExpressionError(`the record has no field ${field}; it has ${Object.keys(value).join(", ")}`);
441
+ return value[field];
442
+ }
443
+ function integer(value, what) {
444
+ if (!value.isInteger()) throw new ExpressionError(`${what} needs a whole number`);
445
+ return Number(value.n);
446
+ }
447
+ function call(fn, args, env) {
448
+ const value = (index) => evaluate(args[index], env);
449
+ const field = () => args[1].name;
450
+ switch (fn) {
451
+ case "round": return asNumber(value(0), "round").round(integer(asNumber(value(1), "round"), "round"));
452
+ case "decimals": {
453
+ const places = asNumber(value(0), "decimals").decimals();
454
+ return Fraction.of(places === Infinity ? 1e6 : places);
455
+ }
456
+ case "map": return asList(value(0), "map").map((item) => fieldOf(item, field()));
457
+ case "sortBy": {
458
+ const name = field();
459
+ return [...asList(value(0), "sortBy")].sort((a, b) => order(fieldOf(a, name), fieldOf(b, name), "sortBy"));
460
+ }
461
+ case "reverse": return [...asList(value(0), "reverse")].reverse();
462
+ default: throw new ExpressionError(`${fn} is not a function of the expression language`);
463
+ }
464
+ }
465
+ /** The value of an expression with the variables of `env`; throws an ExpressionError when it has none. */
466
+ function evaluate(node, env) {
467
+ switch (node.kind) {
468
+ case "number":
469
+ case "string":
470
+ case "boolean": return node.value;
471
+ case "name":
472
+ if (!Object.hasOwn(env, node.name)) throw new ExpressionError(`${node.name} is not declared`);
473
+ return env[node.name];
474
+ case "list": return node.items.map((item) => evaluate(item, env));
475
+ case "field": return fieldOf(evaluate(node.of, env), node.field);
476
+ case "index": {
477
+ const list = asList(evaluate(node.of, env), "[…]");
478
+ const index = integer(asNumber(evaluate(node.index, env), "[…]"), "[…]");
479
+ if (index < 0 || index >= list.length) throw new ExpressionError(`the list has no element ${index}; it has ${list.length}, from 0`);
480
+ return list[index];
481
+ }
482
+ case "call": return call(node.fn, node.args, env);
483
+ case "unary": {
484
+ const arg = evaluate(node.arg, env);
485
+ return node.op === "-" ? asNumber(arg, "-").neg() : !asBoolean(arg, "not");
486
+ }
487
+ case "binary": {
488
+ if (node.op === "and") return asBoolean(evaluate(node.left, env), "and") && asBoolean(evaluate(node.right, env), "and");
489
+ if (node.op === "or") return asBoolean(evaluate(node.left, env), "or") || asBoolean(evaluate(node.right, env), "or");
490
+ const left = evaluate(node.left, env);
491
+ const right = evaluate(node.right, env);
492
+ switch (node.op) {
493
+ case "+": return asNumber(left, "+").add(asNumber(right, "+"));
494
+ case "-": return asNumber(left, "-").sub(asNumber(right, "-"));
495
+ case "*": return asNumber(left, "*").mul(asNumber(right, "*"));
496
+ case "/": {
497
+ const divisor = asNumber(right, "/");
498
+ if (divisor.n === 0n) throw new ExpressionError("division by zero");
499
+ return asNumber(left, "/").div(divisor);
500
+ }
501
+ case "==": return same(left, right);
502
+ case "!=": return !same(left, right);
503
+ default: {
504
+ const sign = order(left, right, node.op);
505
+ return node.op === "<" ? sign < 0 : node.op === "<=" ? sign <= 0 : node.op === ">" ? sign > 0 : sign >= 0;
506
+ }
507
+ }
508
+ }
509
+ }
510
+ }
511
+ // runtime/explainer/generated/comparator.ts
512
+ /** What the comparator of an exercise is. */
513
+ function kindOf(compare) {
514
+ if (compare === "number") return {
515
+ kind: "number",
516
+ decimals: 2,
517
+ tolerance: Fraction.of(0),
518
+ money: false
519
+ };
520
+ if (compare === "order") return { kind: "order" };
521
+ if (compare === "pairs") return { kind: "pairs" };
522
+ if ("number" in compare) {
523
+ const { format = "decimal", decimals, tolerance = 0, unit } = compare.number;
524
+ const places = decimals ?? (format === "integer" ? 0 : 2);
525
+ const shown = unit ?? (format === "money" ? "€" : format === "percent" ? "%" : void 0);
526
+ return {
527
+ kind: "number",
528
+ decimals: places,
529
+ tolerance: Fraction.from(tolerance),
530
+ money: format === "money",
531
+ ...shown === void 0 ? {} : { unit: shown }
532
+ };
533
+ }
534
+ if ("choice" in compare) return {
535
+ kind: "choice",
536
+ options: compare.choice
537
+ };
538
+ return {
539
+ kind: "pairs",
540
+ left: compare.pairs.left,
541
+ right: compare.pairs.right
542
+ };
543
+ }
544
+ // runtime/explainer/generated/texts.ts
545
+ const PATH = String.raw`[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*`;
546
+ /** A placeholder: two braces around a name or a name's field, nothing else inside. */
547
+ const PLACEHOLDER = new RegExp(String.raw`\{\{(${PATH})\}\}`, "g");
548
+ /** What looks meant as a placeholder but is not one: one brace in prose, or spaces inside two braces. */
549
+ const ONE_BRACE = new RegExp(String.raw`(?<!\{)\{(${PATH})\}(?!\})`, "g");
550
+ /** Anything between two braces, as written: a placeholder, or what only looks like one ({{opts[0].x}}, {{}}, {{1x}}). */
551
+ const BRACED = /\{\{([^{}]*)\}\}/g;
552
+ const PATH_ONLY = new RegExp(String.raw`^${PATH}$`);
553
+ const SPACED = new RegExp(String.raw`\{\{\s*(${PATH})\s*\}\}`, "g");
554
+ /** A formula ($…$) or a run of prose, in a template. */
555
+ const SEGMENT = /\$((?:\\.|[^\\$\n])+?)\$|([^$]+)/g;
556
+ const formats = /* @__PURE__ */ new Map();
557
+ /** A number as the page shows it: up to six decimals, no grouping (a learner would type 1500, not 1.500). */
558
+ function formatNumber(value, lang, decimals) {
559
+ const key = `${lang}:${decimals ?? ""}`;
560
+ let format = formats.get(key);
561
+ if (format === void 0) {
562
+ format = new Intl.NumberFormat(localeOf(lang), {
563
+ maximumFractionDigits: decimals ?? 6,
564
+ ...decimals === void 0 ? {} : { minimumFractionDigits: decimals },
565
+ useGrouping: false
566
+ });
567
+ formats.set(key, format);
568
+ }
569
+ return format.format(value.toNumber());
570
+ }
571
+ /** A value as the learner reads it, in `lang`. */
572
+ function label(value, lang) {
573
+ if (value instanceof Fraction) return formatNumber(value, lang);
574
+ if (value instanceof Localized) return value.in(lang);
575
+ if (typeof value === "string") return value;
576
+ if (typeof value === "boolean") return String(value);
577
+ if (Array.isArray(value)) return value.map((item) => label(item, lang)).join(", ");
578
+ throw new ExpressionError("a record cannot be shown; name one of its fields, e.g. {{e.name}}");
579
+ }
580
+ /** The answer as the learner reads it: a sum of money with two decimals when it has any. */
581
+ function answerLabel(spec, task, lang) {
582
+ const kind = kindOf(spec.answer.compare);
583
+ if (kind.kind === "number" && task.answer instanceof Fraction) return formatNumber(task.answer, lang, kind.money && !task.answer.isInteger() ? 2 : void 0);
584
+ return label(task.answer, lang);
585
+ }
586
+ /** A template's placeholders as tokens: {{G}} becomes ⟦G⟧, inside a formula ($…$) {\text{⟦G⟧}}. */
587
+ function tokenize(text) {
588
+ return text.replace(SEGMENT, (whole, math, prose) => math !== void 0 ? `$${math.replace(PLACEHOLDER, (_placeholder, path) => `{\\text{⟦${path}⟧}}`)}$` : (prose ?? whole).replace(PLACEHOLDER, (_placeholder, path) => `⟦${path}⟧`));
589
+ }
590
+ /** The placeholders of a template, by their path (G, e.name, answer). */
591
+ function placeholdersIn(text) {
592
+ return [...text.matchAll(PLACEHOLDER)].map((match) => match[1] ?? "");
593
+ }
594
+ /**
595
+ * The problems of the placeholders of one template: unknown roots, one brace in prose, spaces inside, and double
596
+ * braces around anything but a name or a name's field (an index, a number, nothing). A literal {{ in a formula is
597
+ * written { { }.
598
+ */
599
+ function placeholderProblems(text, known) {
600
+ const problems = [];
601
+ const list = [...known].map((name) => `{{${name}}}`).join(", ");
602
+ for (const path of placeholdersIn(text)) {
603
+ const root = path.split(".")[0] ?? "";
604
+ if (!known.has(root)) problems.push(`{{${path}}} is an unknown placeholder: ${root} is not declared in given; declare it, or use one of ${list}`);
605
+ }
606
+ for (const match of text.matchAll(SPACED)) if (match[0] !== `{{${match[1]}}}`) problems.push(`${match[0]} has spaces inside; a placeholder is written {{${match[1]}}}, without spaces`);
607
+ for (const match of text.matchAll(BRACED)) {
608
+ const inner = match[1] ?? "";
609
+ if (PATH_ONLY.test(inner) || PATH_ONLY.test(inner.trim())) continue;
610
+ problems.push(`${match[0]} is no placeholder: a placeholder is a name or a name's field ({{e.brauch}}); to show one record, pick it on its own (given: { e: { pick: 1, from: { data: feste } } }) and write {{e.brauch}}; a literal {{ in a formula is written { {`);
611
+ }
612
+ for (const match of text.matchAll(SEGMENT)) {
613
+ if (match[2] === void 0) continue;
614
+ for (const one of match[2].matchAll(ONE_BRACE)) {
615
+ const root = (one[1] ?? "").split(".")[0] ?? "";
616
+ if (known.has(root)) problems.push(`{${one[1]}} has one pair of braces; a placeholder is written with two: {{${one[1]}}}`);
617
+ }
618
+ }
619
+ return problems;
620
+ }
621
+ /** The templates of an exercise as written: the task, the steps, the hint and the messages. */
622
+ function writtenTemplates(spec) {
623
+ const written = [];
624
+ const add = (path, text) => {
625
+ if (text === void 0) return;
626
+ if (typeof text === "string") written.push({
627
+ path,
628
+ text
629
+ });
630
+ else for (const [lang, each] of Object.entries(text)) written.push({
631
+ path: [...path, lang],
632
+ lang,
633
+ text: each
634
+ });
635
+ };
636
+ add(["task", "text"], spec.task.text);
637
+ (spec.steps ?? []).forEach((step, index) => add(["steps", index], step));
638
+ add(["hint"], spec.hint);
639
+ (spec.mistakes ?? []).forEach((mistake, index) => add([
640
+ "mistakes",
641
+ index,
642
+ "message"
643
+ ], mistake.message));
644
+ return written;
645
+ }
646
+ /** The problems of the placeholders of an exercise's templates, each at its path. */
647
+ function templateProblems(spec) {
648
+ const declared = [...Object.keys(spec.given), "answer"];
649
+ return writtenTemplates(spec).flatMap(({ path, lang, text }) => {
650
+ return placeholderProblems(text, new Set(declared)).map((message) => ({
651
+ path,
652
+ ...lang === void 0 ? {} : { lang },
653
+ message
654
+ }));
655
+ });
656
+ }
657
+ /**
658
+ * The variables of `given` that the exercise never uses: in no placeholder of its templates, and in none of the
659
+ * expressions of the answer (with the options of a choice) and the mistakes. `where` does not
660
+ * count: a value drawn only to be filtered is never seen.
661
+ */
662
+ function unusedVariables(spec) {
663
+ const used = /* @__PURE__ */ new Set();
664
+ for (const { text } of writtenTemplates(spec)) for (const path of placeholdersIn(text)) used.add(path.split(".")[0] ?? "");
665
+ const expressions = [spec.answer.value, ...(spec.mistakes ?? []).map((mistake) => mistake.value)];
666
+ const compare = spec.answer.compare;
667
+ if (typeof compare === "object") {
668
+ const options = compare["choice"];
669
+ if (typeof options === "string") expressions.push(options);
670
+ }
671
+ for (const source of expressions) try {
672
+ variables(parse(source), used);
673
+ } catch {}
674
+ return Object.keys(spec.given).filter((name) => !used.has(name));
675
+ }
676
+ /** The value of a placeholder's path in a task, or undefined when it has none. */
677
+ function placeholderValue(spec, task, path, lang) {
678
+ if (path === "answer") return {
679
+ text: answerLabel(spec, task, lang),
680
+ ...task.answer instanceof Fraction ? { number: task.answer } : {}
681
+ };
682
+ try {
683
+ const value = evaluate(parse(path), task.values);
684
+ return {
685
+ text: label(value, lang),
686
+ ...value instanceof Fraction ? { number: value } : {}
687
+ };
688
+ } catch {
689
+ return;
690
+ }
691
+ }
692
+ /** A template with the values of a task: HTML (escaped, numbers in formulas as <mn>) or plain text. */
693
+ function fill(template, spec, task, lang, mode = "html") {
694
+ return (mode === "html" ? template.replace(/<mtext>⟦([^⟧]+)⟧<\/mtext>/g, (_whole, path) => {
695
+ const value = placeholderValue(spec, task, path, lang);
696
+ if (value === void 0) return `<mtext>{{${escape(path)}}}</mtext>`;
697
+ if (value.number === void 0) return `<mtext>${escape(value.text)}</mtext>`;
698
+ const negative = value.number.compare(Fraction.of(0)) < 0;
699
+ const digits = escape(negative ? value.text.replace(/^[-−]/, "") : value.text);
700
+ return negative ? `<mo>−</mo><mn>${digits}</mn>` : `<mn>${digits}</mn>`;
701
+ }) : template).replace(/⟦([^⟧]+)⟧/g, (_whole, path) => {
702
+ const value = placeholderValue(spec, task, path, lang);
703
+ if (value === void 0) return `{{${path}}}`;
704
+ return mode === "html" ? escape(value.text) : value.text;
705
+ });
706
+ }
707
+ const templatesIn = (templates, lang) => templates[lang] ?? templates["de"] ?? {
708
+ task: "",
709
+ steps: [],
710
+ messages: []
711
+ };
712
+ /** The task's text as HTML, in `lang`. */
713
+ function taskText(spec, task, lang, templates) {
714
+ return fill(templatesIn(templates, lang).task, spec, task, lang);
715
+ }
716
+ /** The steps of the solution as HTML, in `lang`. */
717
+ function stepsOf(spec, task, lang, templates) {
718
+ return templatesIn(templates, lang).steps.map((step) => fill(step, spec, task, lang));
719
+ }
720
+ /** The hint as HTML, in `lang`, if the exercise has one. */
721
+ function hintOf(spec, task, lang, templates) {
722
+ const hint = templatesIn(templates, lang).hint;
723
+ return hint === void 0 ? void 0 : fill(hint, spec, task, lang);
724
+ }
725
+ /** The text of HTML, as a learner reads it: tags left out, entities resolved, white space folded. */
726
+ function plainText(html) {
727
+ return html.replace(/<[^>]*>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&amp;/g, "&").replace(/\s+/g, " ").trim();
728
+ }
729
+ // runtime/explainer/generated/question.ts
730
+ const listOf = (value, what) => {
731
+ if (!Array.isArray(value)) throw new ExpressionError(`the answer of ${what} is a list`);
732
+ return value;
733
+ };
734
+ /** The task as the question of the comparator's kind, in `lang`. */
735
+ function questionFor(spec, task, lang, templates) {
736
+ const kind = kindOf(spec.answer.compare);
737
+ const base = {
738
+ id: `${spec.id}-${task.seed}`,
739
+ text: plainText(taskText(spec, task, lang, templates)),
740
+ explanation: "",
741
+ links: []
742
+ };
743
+ switch (kind.kind) {
744
+ case "number":
745
+ if (!(task.answer instanceof Fraction)) throw new ExpressionError("the answer of number is a number");
746
+ return {
747
+ ...base,
748
+ type: "number",
749
+ answer: task.answer.toNumber(),
750
+ tolerance: kind.tolerance.toNumber(),
751
+ ...kind.unit === void 0 ? {} : { unit: inLanguage(kind.unit, lang) }
752
+ };
753
+ case "choice": return {
754
+ ...base,
755
+ type: "single-choice",
756
+ options: (task.options ?? []).map((option) => ({
757
+ text: label(option, lang),
758
+ correct: same(task.answer, option),
759
+ info: ""
760
+ }))
761
+ };
762
+ case "order": return {
763
+ ...base,
764
+ type: "ordering",
765
+ items: listOf(task.answer, "order").map((item) => label(item, lang))
766
+ };
767
+ case "pairs": return {
768
+ ...base,
769
+ type: "matching",
770
+ pairs: pairValues(spec, task.answer).map(([left, right]) => ({
771
+ left: label(left, lang),
772
+ right: label(right, lang)
773
+ }))
774
+ };
775
+ }
776
+ }
777
+ /** The pairs of a pairs answer, as values: [left, right]. */
778
+ function pairValues(spec, value) {
779
+ const kind = kindOf(spec.answer.compare);
780
+ return listOf(value, "pairs").map((item) => {
781
+ if (kind.kind === "pairs" && kind.left !== void 0 && kind.right !== void 0) {
782
+ const fields = item;
783
+ return [fields[kind.left] ?? "", fields[kind.right] ?? ""];
784
+ }
785
+ if (!Array.isArray(item) || item.length !== 2) throw new ExpressionError("each pair is a list of two: [left, right]");
786
+ return [item[0], item[1]];
787
+ });
788
+ }
789
+ /** The learner's answer that gives `value` (e.g. a mistake) through the controls of the task's question. */
790
+ function answerGiving(spec, task, value) {
791
+ const kind = kindOf(spec.answer.compare);
792
+ const index = (list, item) => list.findIndex((other) => same(other, item));
793
+ switch (kind.kind) {
794
+ case "number": {
795
+ if (!(value instanceof Fraction)) throw new ExpressionError("a mistake of number is a number");
796
+ const places = Math.min(value.decimals(), Math.max(kind.decimals, 2));
797
+ return formatNumber(value.round(places), "de");
798
+ }
799
+ case "choice": {
800
+ const at = index(task.options ?? [], value);
801
+ if (at < 0) throw new ExpressionError(`${label(value, "de")} is not one of the choices`);
802
+ return at;
803
+ }
804
+ case "order": {
805
+ const order = listOf(value, "order");
806
+ return listOf(task.answer, "order").map((item) => index(order, item));
807
+ }
808
+ case "pairs": {
809
+ const right = pairValues(spec, task.answer);
810
+ const given = pairValues(spec, value);
811
+ return right.map(([left]) => {
812
+ const chosen = given.find(([other]) => same(other, left))?.[1];
813
+ return chosen === void 0 ? -1 : right.findIndex(([, other]) => same(other, chosen));
814
+ });
815
+ }
816
+ }
817
+ }
818
+ /**
819
+ * The learner's answer that gives `value`, an example, through the controls of the task's question: as answerGiving,
820
+ * but a number as it is, with up to six decimals, so an example just outside the tolerance stays outside.
821
+ */
822
+ function exampleGiving(spec, task, value) {
823
+ if (kindOf(spec.answer.compare).kind !== "number") return answerGiving(spec, task, value);
824
+ if (!(value instanceof Fraction)) throw new ExpressionError("an example of number is a number");
825
+ return formatNumber(value, "de");
826
+ }
827
+ /** The learner's answer that gives the mistake `index` of a task, through the controls of its question. */
828
+ function answerFor(spec, task, _question, index) {
829
+ return answerGiving(spec, task, task.mistakes[index]);
830
+ }
831
+ /** Whether two answers to `question` are the same, a number within `slack`. */
832
+ function sameAnswer(question, a, b, slack) {
833
+ if (question.type === "number") {
834
+ const left = typeof a === "string" ? parseNumber(a, question.unit) : void 0;
835
+ const right = typeof b === "string" ? parseNumber(b, question.unit) : void 0;
836
+ return left !== void 0 && right !== void 0 && Math.abs(left - right) <= slack + 1e-9;
837
+ }
838
+ if (Array.isArray(a) && Array.isArray(b)) return a.join() === b.join();
839
+ return a === b;
840
+ }
841
+ /** The index of the first typical mistake the learner's wrong answer makes; undefined when it makes none. */
842
+ function mistakeOf(spec, task, question, answer) {
843
+ if (isCorrect(question, answer)) return void 0;
844
+ const kind = kindOf(spec.answer.compare);
845
+ const slack = kind.kind === "number" ? Math.max(kind.tolerance.toNumber(), .5 * 10 ** -Math.max(kind.decimals, 2)) : 0;
846
+ for (const [index] of task.mistakes.entries()) try {
847
+ if (sameAnswer(question, answer, answerFor(spec, task, question, index), slack)) return index;
848
+ } catch {}
849
+ }
850
+ /** The message of the first mistake the learner's wrong answer makes, in `lang`; undefined when it makes none. */
851
+ function diagnose(spec, task, question, answer, lang, templates) {
852
+ const index = mistakeOf(spec, task, question, answer);
853
+ if (index === void 0) return void 0;
854
+ return fill(templatesIn(templates, lang).messages[index] ?? "", spec, task, lang, "text");
855
+ }
856
+ /** What a learner sees of a task: its text and its question's options, items or pairs; "Neue Aufgabe" changes it. */
857
+ function taskKey(spec, task, lang, templates) {
858
+ const { id: _id, ...question } = questionFor(spec, task, lang, templates);
859
+ return JSON.stringify(question);
860
+ }
861
+ const literal$1 = (value) => typeof value === "number" ? Fraction.from(value) : typeof value === "string" ? value : new Localized(value);
862
+ /** A value of a data record: a map of strings is a text by language, any other map a record. */
863
+ function dataValue(value) {
864
+ if (typeof value === "number") return Fraction.from(value);
865
+ if (typeof value === "boolean" || typeof value === "string") return value;
866
+ if (Array.isArray(value)) return value.map(dataValue);
867
+ return Object.entries(value).every(([, field]) => typeof field === "string") ? new Localized(value) : record(value);
868
+ }
869
+ function record(source) {
870
+ return Object.fromEntries(Object.entries(source).map(([key, value]) => [key, dataValue(value)]));
871
+ }
872
+ function items(source, data) {
873
+ if (Array.isArray(source)) return source.map(literal$1);
874
+ const name = source.data;
875
+ const records = data[name];
876
+ if (records === void 0) throw new ExpressionError(`src/data/${name}.yaml does not exist`);
877
+ return records.map(record);
878
+ }
879
+ /** The value of one arbitrary, drawn with `next`. */
880
+ function draw(arbitrary, next, data) {
881
+ const range = arbitrary.int ?? arbitrary.decimal;
882
+ if (range !== void 0) {
883
+ const [min, max] = range;
884
+ const step = Fraction.from(arbitrary.step ?? 1);
885
+ const low = Fraction.from(min);
886
+ const count = Math.floor(Fraction.from(max).sub(low).div(step).toNumber() + 1e-9) + 1;
887
+ return low.add(step.mul(Fraction.of(Math.floor(next() * count))));
888
+ }
889
+ if (arbitrary.oneOf !== void 0) {
890
+ const list = arbitrary.oneOf;
891
+ return literal$1(list[Math.floor(next() * list.length)]);
892
+ }
893
+ const list = items(arbitrary.from ?? [], data);
894
+ const count = arbitrary.pick ?? 1;
895
+ const picked = [];
896
+ if (arbitrary.distinct === true) {
897
+ const order = shuffled(list.length, Math.floor(next() * 2 ** 31));
898
+ for (const index of order.slice(0, count)) picked.push(list[index]);
899
+ } else for (let i = 0; i < count; i++) picked.push(list[Math.floor(next() * list.length)]);
900
+ return count === 1 && arbitrary.pick === 1 ? picked[0] : picked;
901
+ }
902
+ const compiledCache = /* @__PURE__ */ new WeakMap();
903
+ function compiled(spec) {
904
+ let found = compiledCache.get(spec);
905
+ if (found === void 0) {
906
+ const kind = kindOf(spec.answer.compare);
907
+ found = {
908
+ where: (spec.where ?? []).map((source) => parse(source)),
909
+ answer: parse(spec.answer.value),
910
+ mistakes: (spec.mistakes ?? []).map((mistake) => parse(mistake.value)),
911
+ ...kind.kind === "choice" && typeof kind.options === "string" ? { options: parse(kind.options) } : {}
912
+ };
913
+ compiledCache.set(spec, found);
914
+ }
915
+ return found;
916
+ }
917
+ /** The values of `given` for a seed and an attempt. */
918
+ function values(spec, seed, attempt, data) {
919
+ const next = random(hash(`${spec.id}#${seed}#${attempt}`));
920
+ const drawn = {};
921
+ for (const [name, arbitrary] of Object.entries(spec.given)) drawn[name] = draw(arbitrary, next, data);
922
+ return drawn;
923
+ }
924
+ /** Whether every precondition holds for these values. */
925
+ function holds(spec, env) {
926
+ return compiled(spec).where.every((condition, index) => {
927
+ const value = evaluate(condition, env);
928
+ if (typeof value !== "boolean") throw new ExpressionError(`where[${index}] needs true or false, not a value`);
929
+ return value;
930
+ });
931
+ }
932
+ /** Thrown when no draw of a seed passes `where`. */
933
+ var StarvedError = class extends ExpressionError {};
934
+ /** The task of `seed`: the first draw whose values pass `where`, with the answer, the mistakes and the options. */
935
+ function generate(spec, seed, data) {
936
+ const code = compiled(spec);
937
+ for (let attempt = 0; attempt < 100; attempt++) {
938
+ const env = values(spec, seed, attempt, data);
939
+ if (!holds(spec, env)) continue;
940
+ const task = {
941
+ seed,
942
+ values: env,
943
+ answer: evaluate(code.answer, env),
944
+ mistakes: code.mistakes.map((mistake) => evaluate(mistake, env))
945
+ };
946
+ const kind = kindOf(spec.answer.compare);
947
+ if (kind.kind === "choice") {
948
+ const options = code.options === void 0 ? kind.options.map(literal$1) : evaluate(code.options, env);
949
+ if (!Array.isArray(options)) throw new ExpressionError(`the options of choice are not a list`);
950
+ task.options = [...options];
951
+ }
952
+ return task;
953
+ }
954
+ throw new StarvedError(`where holds in none of 100 draws for seed ${seed}`);
955
+ }
956
+ // runtime/explainer/messages.ts
957
+ const MESSAGES = {
958
+ de: {
959
+ language: "Deutsch",
960
+ languages: "Sprache",
961
+ start: "Start",
962
+ breadcrumbs: "Du bist hier",
963
+ contents: "Inhalt",
964
+ onThisPage: "Auf dieser Seite",
965
+ pages: "Seiten",
966
+ chapters: "Kapitel",
967
+ pager: "Weiterblättern",
968
+ previous: "Zurück: {title}",
969
+ next: "Weiter: {title}",
970
+ glossary: "Glossar",
971
+ glossaryIntro: "Die Begriffe dieser App, von A bis Z, jeder mit der Seite, die ihn erklärt.",
972
+ inGlossary: "im Glossar",
973
+ explainedOn: "Erklärt auf der Seite {page}",
974
+ sources: "Quellen",
975
+ sourcesIntro: "Die Fakten in dieser App stammen aus diesen Quellen:",
976
+ beforeChrist: "{year} v. Chr.",
977
+ code: "Code in {language}",
978
+ footer: "Eine Erklär-App auf lernapps.net",
979
+ imprint: "Impressum",
980
+ privacy: "Datenschutz",
981
+ workedTasks: "Drei Beispielaufgaben, jede mit ihrer Lösung:",
982
+ practice: "Übe selbst, mit immer neuen Aufgaben:",
983
+ newTask: "Neue Aufgabe",
984
+ hint: "Tipp",
985
+ textDescription: "Beschreibung als Text",
986
+ taskNumber: "Aufgabe Nr. {n}",
987
+ backToTutor: "Zurück zum Tutor",
988
+ backToTutorOpen: "Wechsel zurück zum Chat mit {name}.",
989
+ backToTutorCopied: "Wechsel zurück zu {name} und füg es dort ein. Diesen Tab kannst du offen lassen.",
990
+ backToTutorFailed: "Kopieren ging nicht. Kopier die markierten Zeilen, wechsel zurück zu {name} und füg sie dort ein.",
991
+ resultLabel: "Ergebniszeile für deinen Tutor",
992
+ resultLine: "{page}, {exercise}{number}: {status}.",
993
+ resultNumber: ", Nr. {n}",
994
+ resultRight: "richtig im {n}. Versuch",
995
+ resultSolution: "Lösung angesehen",
996
+ resultOpen: "noch nicht gelöst",
997
+ resultTry: "1 Versuch",
998
+ resultTries: "{n} Versuche",
999
+ resultInput: "Eingabe {input}",
1000
+ resultSession: "Sitzung: {right} von {total} richtig.",
1001
+ resultTutor: "Tutor-Seite:",
1002
+ resultTrue: "stimmt",
1003
+ resultFalse: "stimmt nicht",
1004
+ stuckAsk: "Lade {url} und hilf mir bei Übung {exercise}{number}. Ich komme nicht weiter.",
1005
+ stuckTask: "Übung {exercise}{number}:",
1006
+ stuckInput: "Ich habe {input} eingegeben ({tries}).",
1007
+ stuckTries: "Bisher {tries}.",
1008
+ stuckRight: "Ich habe sie im {n}. Versuch richtig gelöst.",
1009
+ stuckMessage: "Die App sagt: „{message}“",
1010
+ stuckHint: "Ich habe den Tipp geöffnet.",
1011
+ stuckSolution: "Ich habe mir die Lösung angesehen.",
1012
+ stuckPrivacy: "„Warum?“ gibt {name} mit deinem Klick die Aufgabe, deine Eingabe und die Meldung der App.",
1013
+ stuckPrivacyCopy: "Dein Klick kopiert die Aufgabe, deine Eingabe und die Meldung der App für den Chat mit deinem Tutor; die App schickt nichts weg.",
1014
+ stuckCopied: "Wechsel zurück zu {name} und füg die Zeilen dort ein.",
1015
+ stuckFailed: "Kopieren ging nicht. Kopier die markierten Zeilen, wechsel zurück zu {name} und füg sie dort ein.",
1016
+ stuckLabel: "Zeilen für deinen Tutor",
1017
+ tutorTitle: "Mit einem KI-Tutor lernen",
1018
+ tutorAbout: "Ein KI-Tutor ist ein KI-Assistent deiner Wahl, der dich beim Lernen mit dieser App begleitet. Er erklärt dir den Stoff mit den Beispielen der Seiten, gibt dir Aufgaben aus der App und hilft dir, wenn du nicht weiterkommst. Gelöst wird in der App: Sie prüft deine Antworten.",
1019
+ tutorAboutStart: "Wähl hier deinen Assistenten. Dann steht er auf jeder Seite unter dem Titel bereit, und jede Übung hat den Knopf „Hilfe“. Du kannst ihn auch auf einer Seite unter dem Titel einschalten.",
1020
+ tutorAboutPrivacy: "Deine Wahl bleibt auf diesem Gerät; erst dein Klick öffnet den Assistenten. Frag deine Eltern oder Lehrkraft, ob du ihn nutzen darfst.",
1021
+ tutorAccount: "Du nutzt dein eigenes Konto beim Anbieter. Der Assistent ist kein Angebot von lernapps.net: lernapps liefert nur Inhalte und Übungen.",
1022
+ tutorScope: "Die Wahl gilt für alle Seiten, nur auf diesem Gerät.",
1023
+ tutorChoose: "Dein KI-Assistent",
1024
+ tutorNone: "Keiner",
1025
+ needsTitle: "Was dir beim Lernen hilft",
1026
+ needsOpen: "Einstellungen",
1027
+ needsDone: "Fertig",
1028
+ needsAbout: "Hier kannst du deinem Tutor sagen, wie er mit dir sprechen soll. Kreuz an, was dir hilft; du kannst mehreres wählen.",
1029
+ needKurzeSchritte: "Kurze Schritte",
1030
+ needKurzeSchritteAbout: "Der Tutor erklärt in kleinen Schritten, immer nur eine Sache auf einmal. Du weißt immer, wie weit es noch ist.",
1031
+ needWenigLesen: "Wenig lesen",
1032
+ needWenigLesenAbout: "Der Tutor schreibt sehr kurz und einfach und erklärt jedes Fachwort. Tipp: Lass dir seine Antworten vorlesen und sprich deine Antwort ein.",
1033
+ needMehrTiefe: "Mehr Tiefe",
1034
+ needMehrTiefeAbout: "Der Tutor überspringt, was du schon kannst, und stellt dir knifflige Warum- und Was-wäre-wenn-Fragen.",
1035
+ needAndereMuttersprache: "Andere Muttersprache",
1036
+ needAndereMutterspracheAbout: "Der Tutor erklärt auf Deutsch in einfachen Sätzen und übersetzt neue Fachwörter in deine Sprache. Du darfst auch in deiner Sprache schreiben.",
1037
+ needKlarUndWoertlich: "Klar und wörtlich",
1038
+ needKlarUndWoertlichAbout: "Der Tutor schreibt genau das, was er meint: ohne Witze, versteckte Bedeutungen oder Redewendungen. Er sagt dir immer, wo ihr gerade seid.",
1039
+ needRechnenAnschaulich: "Rechnen anschaulich",
1040
+ needRechnenAnschaulichAbout: "Der Tutor zeigt dir, wie viel eine Zahl ist, und erklärt in kleinen Schritten. Du rechnest selbst und hast so viel Zeit, wie du brauchst.",
1041
+ needsLanguage: "Deine Muttersprache",
1042
+ needsLanguageNone: "Bitte wählen",
1043
+ needsScopeTitle: "Das gilt",
1044
+ needsScopeAll: "für alle Erklär-Apps auf lernapps.net",
1045
+ needsScopeApp: "nur für diese App",
1046
+ needsPrivacy: "Deine Wahl bleibt auf diesem Gerät. Der Tutor bekommt nur die Namen der Einstellungen, die du ankreuzt, und erst, wenn du ihn öffnest.",
1047
+ tutorOther: "Ein anderer: Text kopieren",
1048
+ tutorOn: "Einschalten",
1049
+ tutorSomeone: "deinen KI-Tutor",
1050
+ help: "Hilfe",
1051
+ helpWhy: "Warum?",
1052
+ helpBack: "Zurück",
1053
+ helpLabel: "{name} um Hilfe bitten",
1054
+ helpWhyLabel: "{name} fragen, warum",
1055
+ helpBackLabel: "Zurück zu {name}",
1056
+ tutorCopy: "Text kopieren",
1057
+ tutorOpenWith: "Mit {name} öffnen",
1058
+ tutorGo: "Mit {name} lernen",
1059
+ tutorWay: "Assistent",
1060
+ tutorPageIntro: "Ein KI-Tutor erklärt dir diese Seite und hilft dir bei den Übungen. Wähl deinen Assistenten:",
1061
+ tutorPageLink: "Ansehen, was dein Tutor zu dieser Seite bekommt",
1062
+ tutorPasteHint: "{name} lädt keine Webseiten, wenn der Link aus einem Knopf kommt. Darum kopieren wir den Satz mit dem Link für dich. Ein Klick auf „Mit {name} lernen“ öffnet {name} mit einer kurzen Nachricht. Füg danach den kopierten Satz ein (Strg+V, am Mac ⌘+V) und schick ihn ab.",
1063
+ tutorDone: "✓ Kopiert – jetzt in {name} einfügen",
1064
+ tutorCopied: "Der Prompt ist kopiert. Füg ihn in den Chat deines KI-Assistenten ein.",
1065
+ tutorCopyFailed: "Kopieren ging nicht. Versuch es noch einmal.",
1066
+ exerciseLabel: "Übung",
1067
+ videoChannel: "{channel} · YouTube",
1068
+ videoNote: "Beim Abspielen gehen Daten an YouTube/Google, darunter deine IP-Adresse.",
1069
+ videoPlay: "Video abspielen",
1070
+ videoAlways: "Videos immer direkt laden",
1071
+ assessmentTitle: "Selbsteinschätzung",
1072
+ assessmentOfferTitle: "Was kannst du schon?",
1073
+ assessmentOfferApp: "Freiwillig: Bevor du anfängst, kannst du {count} Aufgaben aus dieser App lösen. Am Ende siehst du für jede Seite, ob du sie dir genauer ansehen solltest.",
1074
+ assessmentOfferChapter: "Freiwillig: Bevor du anfängst, kannst du {count} Aufgaben aus diesem Kapitel lösen. Am Ende siehst du für jede Seite, ob du sie dir genauer ansehen solltest.",
1075
+ assessmentOfferStart: "Selbsteinschätzung starten",
1076
+ assessmentOfferLast: "Dein letztes Ergebnis ansehen",
1077
+ assessmentIntro: "Mit dieser Selbsteinschätzung bekommst du mit {count} Aufgaben einen Überblick, was du schon kannst.",
1078
+ assessmentAllAsked: "Sie fragt jede Seite ab.",
1079
+ assessmentLeftOut: "{n} Seiten fragt sie nicht ab. Es lohnt sich also, sie einmal anzuschauen:",
1080
+ assessmentLeftOutOne: "Eine Seite fragt sie nicht ab. Es lohnt sich also, sie einmal anzuschauen:",
1081
+ assessmentHow: "Die App sagt dir nicht bei jeder Aufgabe, ob sie stimmt. Erst am Ende siehst du für jede abgefragte Seite, ob du sie schon kannst. Was du leer lässt, zählt als nicht gekonnt.",
1082
+ assessmentPrivacy: "Dein Ergebnis bleibt nur auf diesem Gerät. Ein neuer Start ersetzt es.",
1083
+ assessmentBegin: "Los geht's",
1084
+ assessmentStep: "Seite {n} von {count}",
1085
+ assessmentTask: "Aufgabe {n}",
1086
+ assessmentBack: "Zurück",
1087
+ assessmentNext: "Weiter",
1088
+ assessmentFinish: "Auswerten",
1089
+ assessmentResultTitle: "Dein Ergebnis",
1090
+ assessmentScore: "Du hast {right} von {total} Aufgaben richtig gelöst.",
1091
+ assessmentScorePartly: "Du hast {right} von {total} Aufgaben richtig gelöst und {partly} teilweise.",
1092
+ assessmentAdvice: "Fang mit den Seiten an, die du genau lesen solltest.",
1093
+ assessmentAllKnown: "Die abgefragten Seiten kannst du wahrscheinlich schon. Üb trotzdem auf den Seiten, damit es sicher sitzt.",
1094
+ assessmentKnown: "Kannst du wahrscheinlich schon",
1095
+ assessmentPartly: "Schau sie dir noch einmal an",
1096
+ assessmentRead: "Lies sie genau",
1097
+ assessmentUnasked: "Nicht abgefragt",
1098
+ assessmentPageScore: "{right} von {total} richtig",
1099
+ assessmentPageScorePartly: "{right} von {total} richtig, {partly} teilweise",
1100
+ assessmentRestart: "Neu starten",
1101
+ assessmentDelete: "Ergebnis löschen",
1102
+ assessmentDeleted: "Das Ergebnis ist gelöscht.",
1103
+ assessmentTutorTitle: "Mit einem KI-Tutor weiterlernen",
1104
+ assessmentTutorIntro: "Ein KI-Tutor kann mit deinem Ergebnis die Seiten mit dir durchgehen, die noch nicht sitzen. Dein Klick gibt ihm das Ergebnis und deine Eingaben; sonst schickt die App nichts weg.",
1105
+ assessmentTutorGo: "Mit {name} besprechen",
1106
+ assessmentTutorPage: "Ansehen, was dein Tutor bekommt",
1107
+ assessmentNojs: "Die Selbsteinschätzung braucht JavaScript.",
1108
+ assessmentAsk: "Lade {url} und sei mein Tutor. Ich habe die Selbsteinschätzung gemacht. Mein Ergebnis je Seite:",
1109
+ assessmentLine: "{key} ({page}, Übung {exercise}{number}): {state}.",
1110
+ assessmentRight: "richtig",
1111
+ assessmentPartlyRight: "teilweise richtig",
1112
+ assessmentWrong: "falsch",
1113
+ assessmentOpen: "nicht beantwortet",
1114
+ assessmentInput: "Eingabe: {input}",
1115
+ assessmentMissed: "Nicht erreicht: {points}.",
1116
+ assessmentPageLine: "„{page}“: {band} ({right} von {total} richtig).",
1117
+ assessmentPageLinePartly: "„{page}“: {band} ({right} von {total} richtig, {partly} teilweise).",
1118
+ assessmentPageUnasked: "„{page}“: nicht abgefragt.",
1119
+ assessmentTasks: "Meine Aufgaben:",
1120
+ assessmentBackIntro: "Drück „Zurück zum Tutor“. Das kopiert dein Ergebnis. Füg es im Chat mit deinem Tutor ein.",
1121
+ assessmentBackLine: "Ich habe die Selbsteinschätzung gemacht. Mein Ergebnis je Seite:",
1122
+ assessmentTutorRun: "Diese Runde ist für deinen Tutor: Die App speichert nichts davon.",
1123
+ sessionIntro: "Ich habe auf {url} geübt:",
1124
+ sessionNone: "Ich habe auf {url} noch keine Aufgabe gelöst.",
1125
+ sessionTask: "{exercise}{number}: {status}.",
1126
+ assessmentNextRound: "Link für eine neue Runde: {url}",
1127
+ assessmentLinks: "Links für meinen Tutor, genau diese nehmen:",
1128
+ assessmentLinkChapter: "Kapitel „{chapter}“, Selbsteinschätzung: {url}",
1129
+ assessmentLinkChapterNone: "Kapitel „{chapter}“ (keine Selbsteinschätzung):",
1130
+ assessmentLinkPage: "„{page}“: {url}",
1131
+ assessmentLinkWhole: "Selbsteinschätzung der ganzen App: {url}",
1132
+ backToTutorDone: "✓ Kopiert",
1133
+ tutorSomeoneTo: "deinem KI-Tutor",
1134
+ openTaskLabel: "Offene Aufgabe",
1135
+ openTaskPoints: "Eine gute Antwort enthält:",
1136
+ openTaskForm: "Form",
1137
+ openTaskSource: "Nachlesen",
1138
+ openTaskChoice: "Deine Lösung",
1139
+ openTaskType: "Hier tippen",
1140
+ openTaskPaper: "Auf Papier, dann Foto",
1141
+ openTaskAnswer: "Deine Antwort",
1142
+ openTaskPaperHow: "Schreib deine Antwort auf ein Blatt. Dann fotografiere es.",
1143
+ openTaskPhoto: "Blatt fotografieren",
1144
+ openTaskPhotoAgain: "Neues Foto",
1145
+ openTaskPhotoAlt: "Foto deines Blatts",
1146
+ openTaskPhotoLocal: "Das Foto bleibt auf diesem Gerät: Die App speichert und sendet es nicht.",
1147
+ openTaskCompare: "Vergleichen",
1148
+ openTaskWriteFirst: "Schreib zuerst deine Antwort. Dann kannst du vergleichen.",
1149
+ openTaskPhotoFirst: "Fotografiere zuerst dein Blatt. Dann kannst du vergleichen.",
1150
+ openTaskCheck: "Was steht in deiner Antwort?",
1151
+ openTaskCheckHow: "Hake ab, was du wirklich geschrieben hast.",
1152
+ openTaskChange: "Antwort ändern",
1153
+ openTaskLocked: "Die Lösung öffnet sich nach deiner Antwort.",
1154
+ resultPartly: "teilweise richtig ({n} von {total} Punkten, es fehlen {missed})",
1155
+ resultPhoto: "Antwort als Foto",
1156
+ resultAnswer: "Antwort „{input}“"
1157
+ },
1158
+ en: {
1159
+ language: "English",
1160
+ languages: "Language",
1161
+ start: "Start",
1162
+ breadcrumbs: "You are here",
1163
+ contents: "Contents",
1164
+ onThisPage: "On this page",
1165
+ pages: "Pages",
1166
+ chapters: "Chapters",
1167
+ pager: "Page through",
1168
+ previous: "Back: {title}",
1169
+ next: "Next: {title}",
1170
+ glossary: "Glossary",
1171
+ glossaryIntro: "The terms of this app, from A to Z, each with the page that explains it.",
1172
+ inGlossary: "in the glossary",
1173
+ explainedOn: "Explained on the page {page}",
1174
+ sources: "Sources",
1175
+ sourcesIntro: "The facts in this app come from these sources:",
1176
+ beforeChrist: "{year} BC",
1177
+ code: "Code in {language}",
1178
+ footer: "An explainer on lernapps.net",
1179
+ imprint: "Imprint",
1180
+ privacy: "Privacy",
1181
+ workedTasks: "Three worked tasks, each with its solution:",
1182
+ practice: "Practise yourself, with new tasks each time:",
1183
+ newTask: "New task",
1184
+ hint: "Hint",
1185
+ textDescription: "Text description",
1186
+ taskNumber: "Task no. {n}",
1187
+ backToTutor: "Back to the tutor",
1188
+ backToTutorOpen: "Switch back to the chat with {name}.",
1189
+ backToTutorCopied: "Switch back to {name} and paste it there. You may leave this tab open.",
1190
+ backToTutorFailed: "Copying did not work. Copy the selected lines, switch back to {name} and paste them there.",
1191
+ resultLabel: "Result line for your tutor",
1192
+ resultLine: "{page}, {exercise}{number}: {status}.",
1193
+ resultNumber: ", no. {n}",
1194
+ resultRight: "right on try {n}",
1195
+ resultSolution: "solution viewed",
1196
+ resultOpen: "not solved yet",
1197
+ resultTry: "1 try",
1198
+ resultTries: "{n} tries",
1199
+ resultInput: "input {input}",
1200
+ resultSession: "Session: {right} of {total} right.",
1201
+ resultTutor: "Tutor page:",
1202
+ resultTrue: "true",
1203
+ resultFalse: "false",
1204
+ stuckAsk: "Load {url} and help me with exercise {exercise}{number}. I am stuck.",
1205
+ stuckTask: "Exercise {exercise}{number}:",
1206
+ stuckInput: "I entered {input} ({tries}).",
1207
+ stuckTries: "So far {tries}.",
1208
+ stuckRight: "I solved it on try {n}.",
1209
+ stuckMessage: "The app says: “{message}”",
1210
+ stuckHint: "I opened the hint.",
1211
+ stuckSolution: "I viewed the solution.",
1212
+ stuckPrivacy: "With your click, “Why?” gives {name} the task, your input and the app's message.",
1213
+ stuckPrivacyCopy: "Your click copies the task, your input and the app's message for the chat with your tutor; the app sends nothing anywhere.",
1214
+ stuckCopied: "Switch back to {name} and paste the lines there.",
1215
+ stuckFailed: "Copying did not work. Copy the selected lines, switch back to {name} and paste them there.",
1216
+ stuckLabel: "Lines for your tutor",
1217
+ tutorTitle: "Learn with an AI tutor",
1218
+ tutorAbout: "An AI tutor is an AI assistant of your choice that accompanies you while you learn with this app. It explains the topic with the examples of the pages, gives you tasks from the app and helps you when you are stuck. You solve the tasks in the app: it checks your answers.",
1219
+ tutorAboutStart: "Choose your assistant here. It is then ready under the title of every page, and every exercise has the button “Help”. You can also switch it on under the title of a page.",
1220
+ tutorAboutPrivacy: "Your choice stays on this device; only your click opens the assistant. Ask your parents or teacher whether you may use it.",
1221
+ tutorAccount: "You use your own account with the provider. The assistant is not offered by lernapps.net: lernapps only provides the content and the exercises.",
1222
+ tutorScope: "The choice holds for all pages, on this device only.",
1223
+ tutorChoose: "Your AI assistant",
1224
+ tutorNone: "None",
1225
+ needsTitle: "What helps you learn",
1226
+ needsOpen: "Settings",
1227
+ needsDone: "Done",
1228
+ needsAbout: "Here you can tell your tutor how it should talk to you. Tick what helps you; you can choose several.",
1229
+ needKurzeSchritte: "Short steps",
1230
+ needKurzeSchritteAbout: "The tutor explains in small steps, only one thing at a time. You always know how far there is still to go.",
1231
+ needWenigLesen: "Little to read",
1232
+ needWenigLesenAbout: "The tutor writes very briefly and simply and explains every technical term. Tip: let it read its answers out to you and speak your answer.",
1233
+ needMehrTiefe: "More depth",
1234
+ needMehrTiefeAbout: "The tutor skips what you already know and asks you tricky why and what-if questions.",
1235
+ needAndereMuttersprache: "Another first language",
1236
+ needAndereMutterspracheAbout: "The tutor explains in simple sentences in the language of the app and translates new technical terms into your language. You may also write in your language.",
1237
+ needKlarUndWoertlich: "Clear and literal",
1238
+ needKlarUndWoertlichAbout: "The tutor writes exactly what it means: no jokes, hidden meanings or idioms. It always tells you where you are.",
1239
+ needRechnenAnschaulich: "Numbers you can see",
1240
+ needRechnenAnschaulichAbout: "The tutor shows you how much a number is and explains in small steps. You work things out yourself and take as much time as you need.",
1241
+ needsLanguage: "Your first language",
1242
+ needsLanguageNone: "Please choose",
1243
+ needsScopeTitle: "This holds",
1244
+ needsScopeAll: "for every explainer app on lernapps.net",
1245
+ needsScopeApp: "for this app only",
1246
+ needsPrivacy: "Your choice stays on this device. The tutor gets only the names of the settings you tick, and only when you open it.",
1247
+ tutorOther: "Another one: copy the text",
1248
+ tutorOn: "Switch on",
1249
+ tutorSomeone: "your AI tutor",
1250
+ help: "Help",
1251
+ helpWhy: "Why?",
1252
+ helpBack: "Back",
1253
+ helpLabel: "Ask {name} for help",
1254
+ helpWhyLabel: "Ask {name} why",
1255
+ helpBackLabel: "Back to {name}",
1256
+ tutorCopy: "Copy the text",
1257
+ tutorOpenWith: "Open with {name}",
1258
+ tutorGo: "Learn with {name}",
1259
+ tutorWay: "Assistant",
1260
+ tutorPageIntro: "An AI tutor explains this page to you and helps you with the exercises. Choose your assistant:",
1261
+ tutorPageLink: "See what your tutor gets for this page",
1262
+ tutorPasteHint: "{name} does not load web pages when the link comes from a button. So we copy the sentence with the link for you. A click on “Learn with {name}” opens {name} with a short message. Then paste the copied sentence (Ctrl+V, ⌘+V on a Mac) and send it.",
1263
+ tutorDone: "✓ Copied – now paste it into {name}",
1264
+ tutorCopied: "The prompt is copied. Paste it into the chat of your AI assistant.",
1265
+ tutorCopyFailed: "Copying did not work. Try again.",
1266
+ exerciseLabel: "Exercise",
1267
+ videoChannel: "{channel} · YouTube",
1268
+ videoNote: "Playing it sends data to YouTube/Google, among them your IP address.",
1269
+ videoPlay: "Play video",
1270
+ videoAlways: "Always load videos directly",
1271
+ assessmentTitle: "Self-assessment",
1272
+ assessmentOfferTitle: "What do you know already?",
1273
+ assessmentOfferApp: "Optional: before you start, you can solve {count} tasks from this app. At the end you see for each page whether you should look at it more closely.",
1274
+ assessmentOfferChapter: "Optional: before you start, you can solve {count} tasks from this chapter. At the end you see for each page whether you should look at it more closely.",
1275
+ assessmentOfferStart: "Start the self-assessment",
1276
+ assessmentOfferLast: "See your last result",
1277
+ assessmentIntro: "With this self-assessment, {count} tasks give you an overview of what you know already.",
1278
+ assessmentAllAsked: "It asks about every page.",
1279
+ assessmentLeftOut: "It does not ask about {n} pages. So they are worth a look:",
1280
+ assessmentLeftOutOne: "It does not ask about one page. So it is worth a look:",
1281
+ assessmentHow: "The app does not tell you for each task whether it is right. Only at the end do you see for each page asked whether you know it already. What you leave empty counts as not known.",
1282
+ assessmentPrivacy: "Your result stays on this device only. A new start replaces it.",
1283
+ assessmentBegin: "Let's go",
1284
+ assessmentStep: "Page {n} of {count}",
1285
+ assessmentTask: "Task {n}",
1286
+ assessmentBack: "Back",
1287
+ assessmentNext: "Next",
1288
+ assessmentFinish: "Evaluate",
1289
+ assessmentResultTitle: "Your result",
1290
+ assessmentScore: "You solved {right} of {total} tasks right.",
1291
+ assessmentScorePartly: "You solved {right} of {total} tasks right and {partly} partly.",
1292
+ assessmentAdvice: "Start with the pages you should read closely.",
1293
+ assessmentAllKnown: "You probably know the pages asked already. Practise on the pages anyway, so that it sticks.",
1294
+ assessmentKnown: "You probably know it already",
1295
+ assessmentPartly: "Look at it again",
1296
+ assessmentRead: "Read it closely",
1297
+ assessmentUnasked: "Not asked",
1298
+ assessmentPageScore: "{right} of {total} right",
1299
+ assessmentPageScorePartly: "{right} of {total} right, {partly} partly",
1300
+ assessmentRestart: "Start again",
1301
+ assessmentDelete: "Delete the result",
1302
+ assessmentDeleted: "The result is deleted.",
1303
+ assessmentTutorTitle: "Go on learning with an AI tutor",
1304
+ assessmentTutorIntro: "With your result, an AI tutor can go through the pages with you that do not stick yet. Your click gives it the result and your inputs; apart from that the app sends nothing.",
1305
+ assessmentTutorGo: "Talk it over with {name}",
1306
+ assessmentTutorPage: "See what your tutor gets",
1307
+ assessmentNojs: "The self-assessment needs JavaScript.",
1308
+ assessmentAsk: "Load {url} and be my tutor. I did the self-assessment. My result per page:",
1309
+ assessmentLine: "{key} ({page}, exercise {exercise}{number}): {state}.",
1310
+ assessmentRight: "right",
1311
+ assessmentPartlyRight: "partly right",
1312
+ assessmentWrong: "wrong",
1313
+ assessmentOpen: "not answered",
1314
+ assessmentInput: "Input: {input}",
1315
+ assessmentMissed: "Not reached: {points}.",
1316
+ assessmentPageLine: "“{page}”: {band} ({right} of {total} right).",
1317
+ assessmentPageLinePartly: "“{page}”: {band} ({right} of {total} right, {partly} partly).",
1318
+ assessmentPageUnasked: "“{page}”: not asked.",
1319
+ assessmentTasks: "My tasks:",
1320
+ assessmentBackIntro: "Press “Back to the tutor”. It copies your result. Paste it into the chat with your tutor.",
1321
+ assessmentBackLine: "I did the self-assessment. My result per page:",
1322
+ assessmentTutorRun: "This round is for your tutor: the app keeps nothing of it.",
1323
+ sessionIntro: "I practised on {url}:",
1324
+ sessionNone: "I have not solved a task on {url} yet.",
1325
+ sessionTask: "{exercise}{number}: {status}.",
1326
+ assessmentNextRound: "Link for a new round: {url}",
1327
+ assessmentLinks: "Links for my tutor, take exactly these:",
1328
+ assessmentLinkChapter: "Chapter “{chapter}”, self-assessment: {url}",
1329
+ assessmentLinkChapterNone: "Chapter “{chapter}” (no self-assessment):",
1330
+ assessmentLinkPage: "“{page}”: {url}",
1331
+ assessmentLinkWhole: "Self-assessment of the whole app: {url}",
1332
+ backToTutorDone: "✓ Copied",
1333
+ tutorSomeoneTo: "your AI tutor",
1334
+ openTaskLabel: "Open task",
1335
+ openTaskPoints: "A good answer has:",
1336
+ openTaskForm: "form",
1337
+ openTaskSource: "Read up",
1338
+ openTaskChoice: "Your answer",
1339
+ openTaskType: "Type it here",
1340
+ openTaskPaper: "On paper, then a photo",
1341
+ openTaskAnswer: "Your answer",
1342
+ openTaskPaperHow: "Write your answer on a sheet of paper. Then take a photo of it.",
1343
+ openTaskPhoto: "Take a photo of the sheet",
1344
+ openTaskPhotoAgain: "New photo",
1345
+ openTaskPhotoAlt: "Photo of your sheet",
1346
+ openTaskPhotoLocal: "The photo stays on this device: the app neither keeps nor sends it.",
1347
+ openTaskCompare: "Compare",
1348
+ openTaskWriteFirst: "Write your answer first. Then you can compare.",
1349
+ openTaskPhotoFirst: "Take a photo of your sheet first. Then you can compare.",
1350
+ openTaskCheck: "What is in your answer?",
1351
+ openTaskCheckHow: "Tick what you really wrote.",
1352
+ openTaskChange: "Change the answer",
1353
+ openTaskLocked: "The solution opens after your answer.",
1354
+ resultPartly: "partly right ({n} of {total} points, missing {missed})",
1355
+ resultPhoto: "answer as a photo",
1356
+ resultAnswer: "answer “{input}”"
1357
+ }
1358
+ };
1359
+ /** The text of `key` in `lang`, with its placeholders filled. */
1360
+ function message(lang, key, params = {}) {
1361
+ return textsIn(MESSAGES, lang)[key].replace(/\{(\w+)\}/g, (placeholder, name) => params[name] ?? placeholder);
1362
+ }
1363
+ // runtime/explainer/video.ts
1364
+ const YOUTUBE_ID = /^[A-Za-z0-9_-]{11}$/;
1365
+ /** Whether `id` is a YouTube id: 11 letters, digits, - or _. */
1366
+ const isYoutubeId = (id) => typeof id === "string" && YOUTUBE_ID.test(id);
1367
+ function checked(id) {
1368
+ if (!isYoutubeId(id)) throw new Error(`${JSON.stringify(id)} is no YouTube id`);
1369
+ return id;
1370
+ }
1371
+ /** The video's page on YouTube, which the link opens: no request before the learner clicks it. */
1372
+ const watchAddress = (id) => `https://www.youtube.com/watch?v=${checked(id)}`;
1373
+ /** The player of the video on youtube-nocookie.com, which starts at once when the learner clicked for it. */
1374
+ const embedAddress = (id, autoplay) => `https://www.youtube-nocookie.com/embed/${checked(id)}${autoplay ? "?autoplay=1" : ""}`;
1375
+ /** A text as Markdown that shows it as written: its brackets, emphasis, code, formulas and HTML escaped. */
1376
+ const literal = (text) => text.replace(/\s+/g, " ").trim().replace(/[\\`*_[\]<&$]/g, "\\$&");
1377
+ /**
1378
+ * The video as a KI tutor reads it in the page's content (tutor.ts): one line, `word` (e.g. "Video"), the title as a
1379
+ * link to its page on YouTube, and the channel. A source the tutor can name; it cannot watch the video.
1380
+ */
1381
+ const videoLine = (block, word) => `${word}: [${literal(block.title)}](${watchAddress(block.youtube)}), ${literal(block.channel)} (YouTube)`;
1382
+ /** The key "Videos immer direkt laden" is kept under on the device, for every video of the app. */
1383
+ const VIDEO_KEY = "videos-direct";
1384
+ /**
1385
+ * A video block as the build writes it: a figure with the title as a link to YouTube and the channel as its caption,
1386
+ * the note on privacy, and the button and the choice, hidden until the client module runs. `storage` is the app's
1387
+ * name for what it keeps on the device.
1388
+ */
1389
+ function renderVideo(block, lang, storage) {
1390
+ const t = (key) => escape(message(lang, key));
1391
+ return [
1392
+ `<figure class="explainer-video" data-youtube="${escape(block.youtube)}" data-storage="${escape(storage)}">`,
1393
+ `<figcaption><a class="explainer-video-link" href="${escape(watchAddress(block.youtube))}" rel="noopener">${escape(block.title)}</a>`,
1394
+ ` <span class="explainer-video-channel">${escape(message(lang, "videoChannel", { channel: block.channel }))}</span></figcaption>`,
1395
+ `<p class="explainer-video-note">${t("videoNote")}</p>`,
1396
+ `<p class="explainer-video-controls" hidden><button type="button" class="explainer-video-play">${t("videoPlay")}</button>`,
1397
+ ` <label class="explainer-video-always"><input type="checkbox"> ${t("videoAlways")}</label></p>`,
1398
+ `</figure>`
1399
+ ].join("");
1400
+ }
1401
+ const isTrue = (value) => value === true;
1402
+ /**
1403
+ * Puts the player of the video into its figure, above the caption, in place of the button. After a click it starts
1404
+ * at once and gets the focus; loaded directly, it waits to be scrolled near and to be started.
1405
+ */
1406
+ function loadPlayer(figure, id, clicked) {
1407
+ if (figure.querySelector("iframe")) return;
1408
+ const document = figure.ownerDocument;
1409
+ const frame = document.createElement("div");
1410
+ frame.className = "explainer-video-frame";
1411
+ const player = document.createElement("iframe");
1412
+ player.src = embedAddress(id, clicked);
1413
+ player.title = figure.querySelector(".explainer-video-link")?.textContent ?? "";
1414
+ player.allow = "autoplay; encrypted-media; picture-in-picture; fullscreen";
1415
+ player.allowFullscreen = true;
1416
+ player.referrerPolicy = "strict-origin-when-cross-origin";
1417
+ if (!clicked) player.loading = "lazy";
1418
+ frame.append(player);
1419
+ figure.prepend(frame);
1420
+ figure.querySelector(".explainer-video-play")?.remove();
1421
+ if (clicked) player.focus();
1422
+ }
1423
+ /**
1424
+ * Starts every video of the page: the button and the choice show; the button loads the player. The choice is kept
1425
+ * on the device as soon as the learner ticks it, goes as soon as they untick it, and is shown alike on every video of
1426
+ * the page; while it is kept, each player loads without the click.
1427
+ */
1428
+ function startVideos(root) {
1429
+ const figures = [...root.querySelectorAll("figure.explainer-video")];
1430
+ const boxes = figures.flatMap((figure) => [...figure.querySelectorAll(".explainer-video-always input")]);
1431
+ for (const figure of figures) {
1432
+ const id = figure.dataset["youtube"];
1433
+ const controls = figure.querySelector(".explainer-video-controls");
1434
+ if (!isYoutubeId(id) || !controls) continue;
1435
+ const storage = createStorage(figure.dataset["storage"] ?? "explainer");
1436
+ const box = controls.querySelector("input");
1437
+ const direct = storage.load(VIDEO_KEY, isTrue) === true;
1438
+ if (box) {
1439
+ box.checked = direct;
1440
+ box.addEventListener("change", () => {
1441
+ if (box.checked) storage.save(VIDEO_KEY, true);
1442
+ else storage.remove(VIDEO_KEY);
1443
+ for (const other of boxes) other.checked = box.checked;
1444
+ });
1445
+ }
1446
+ controls.querySelector(".explainer-video-play")?.addEventListener("click", () => loadPlayer(figure, id, true), { once: true });
1447
+ controls.hidden = false;
1448
+ if (direct) loadPlayer(figure, id, false);
1449
+ }
1450
+ }
1451
+ // runtime/explainer/content.ts
1452
+ /** The events of a timeline: its own, or the records of its data file. */
1453
+ function timelineEvents(site, block) {
1454
+ if (block.events !== void 0) return block.events;
1455
+ return site.data[block.data ?? ""] ?? [];
1456
+ }
1457
+ /** Every block of `blocks`, depth first, with the blocks inside sections and boxes. */
1458
+ function* allBlocks(blocks) {
1459
+ for (const block of blocks) {
1460
+ yield block;
1461
+ if (block.type === "section" || block.type === "example" || block.type === "note") yield* allBlocks(block.blocks);
1462
+ }
1463
+ }
1464
+ /** Whether a block is an exercise. */
1465
+ const isExercise = (block) => block.type === "generated" || block.type === "open-task" || [
1466
+ "single-choice",
1467
+ "multiple-choice",
1468
+ "true-false",
1469
+ "number",
1470
+ "ordering",
1471
+ "matching"
1472
+ ].includes(block.type);
1473
+ /** The anchor of a term in the glossary: the German term in lower case, umlauts spelled out, hyphens for the rest. */
1474
+ function termId(term) {
1475
+ return inLanguage(term, "de").toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1476
+ }
1477
+ // runtime/explainer/assessment.ts
1478
+ /** The folder of a self-assessment: einschaetzung/ for the app, <chapter>/einschaetzung/ for a chapter. */
1479
+ const ASSESSMENT = "einschaetzung";
1480
+ /** The address of a self-assessment within its language tree: of the app, or of the chapter `chapter`. */
1481
+ const assessmentPathOf = (chapter) => chapter === void 0 ? `${ASSESSMENT}/` : `${chapter}/${ASSESSMENT}/`;
1482
+ /** The key its last result is kept under on the device. */
1483
+ const assessmentKeyOf = (chapter) => chapter === void 0 ? "self-assessment" : `self-assessment:${chapter}`;
1484
+ /** The parts of an exercise's reference /<chapter>/<page>/#<exercise>, if it has that shape. */
1485
+ function parseRef(ref) {
1486
+ const match = /^\/([a-z0-9-]+)\/([a-z0-9-]+)\/#([a-z0-9-]+)$/.exec(ref);
1487
+ return match === null ? void 0 : {
1488
+ chapter: match[1] ?? "",
1489
+ page: match[2] ?? "",
1490
+ exercise: match[3] ?? ""
1491
+ };
1492
+ }
1493
+ /**
1494
+ * The exercise of `page` with the id `id`: a question of the question types, generated, or an exercise component
1495
+ * (of the component uses `components`, by its anchor); none for anything else.
1496
+ */
1497
+ function exerciseOf(components, page, id) {
1498
+ const blocks = [...allBlocks(page.blocks)];
1499
+ for (const block of blocks) if (isExercise(block) && block.id === id) return block;
1500
+ return components.find((candidate) => candidate.component.exercise && candidate.id === id && blocks.some((block) => block === candidate.block))?.block;
1501
+ }
1502
+ /** Whether each listing of `block` draws a task of its own: a generated exercise, or an exercise component. */
1503
+ const drawsTasks = (block) => block.type === "generated" || block.type === "vue-component";
1504
+ /** The self-assessments of a valid site, the app's first, then each chapter's in order. */
1505
+ function assessments(site) {
1506
+ const of = (source, pages, chapter) => {
1507
+ const items = [];
1508
+ source.pages.forEach((step, index) => {
1509
+ for (const ref of step.exercises) {
1510
+ const parts = parseRef(ref);
1511
+ const page = pages.find((candidate) => candidate.chapter === parts?.chapter && candidate.id === parts.page);
1512
+ const block = page === void 0 || parts === void 0 ? void 0 : exerciseOf(site.components, page, parts.exercise);
1513
+ if (page === void 0 || parts === void 0 || block === void 0) continue;
1514
+ items.push({
1515
+ key: `a${items.length + 1}`,
1516
+ ref,
1517
+ page,
1518
+ exercise: parts.exercise,
1519
+ block,
1520
+ step: index
1521
+ });
1522
+ }
1523
+ });
1524
+ return {
1525
+ ...chapter === void 0 ? {} : { chapter },
1526
+ local: assessmentPathOf(chapter?.id),
1527
+ key: assessmentKeyOf(chapter?.id),
1528
+ pages,
1529
+ items,
1530
+ steps: source.pages.length
1531
+ };
1532
+ };
1533
+ const steps = site.chapters.flatMap((chapter) => chapter.selfAssessment?.pages ?? []);
1534
+ if (steps.length === 0) return [];
1535
+ const all = site.chapters.flatMap((chapter) => chapter.pages);
1536
+ const own = site.chapters.length === 1 ? [] : site.chapters;
1537
+ return [of({ pages: steps }, all), ...own.flatMap((chapter) => chapter.selfAssessment === void 0 ? [] : [of(chapter.selfAssessment, chapter.pages, chapter)])];
1538
+ }
1539
+ /** The self-assessment of the app (no chapter) or of the chapter `chapter`, if it has one. */
1540
+ const assessmentOf = (site, chapter) => assessments(site).find((assessment) => assessment.chapter?.id === chapter);
1541
+ /** The fewest and the most exercises a page of a self-assessment should show. */
1542
+ const STEP_SIZE = {
1543
+ min: 2,
1544
+ max: 5
1545
+ };
1546
+ /**
1547
+ * The note of the build on the pages of the chapters' self-assessments with fewer or more exercises than STEP_SIZE,
1548
+ * each by its file and place, or none: a hint, no problem.
1549
+ */
1550
+ function stepHint(site) {
1551
+ const off = site.chapters.flatMap((chapter) => (chapter.selfAssessment?.pages ?? []).flatMap((step, index) => {
1552
+ const count = step.exercises.length;
1553
+ return count < STEP_SIZE.min || count > STEP_SIZE.max ? [`src/${chapter.id}/chapter.yaml: selfAssessment.pages[${index}] (${count})`] : [];
1554
+ }));
1555
+ if (off.length === 0) return void 0;
1556
+ return `lernapps explainer: ${off.length} page${off.length === 1 ? "" : "s"} of a self-assessment show${off.length === 1 ? "s" : ""} fewer than ${STEP_SIZE.min} or more than ${STEP_SIZE.max} exercises: ${off.join(", ")}`;
1557
+ }
1558
+ const STATES = [
1559
+ "richtig",
1560
+ "teilweise",
1561
+ "falsch",
1562
+ "offen"
1563
+ ];
1564
+ /** Whether `value` is a result as the device keeps it. */
1565
+ function isStoredResult(value) {
1566
+ if (typeof value !== "object" || value === null) return false;
1567
+ const { version, items } = value;
1568
+ return version === 1 && Array.isArray(items) && items.every((item) => typeof item === "object" && typeof item.key === "string" && typeof item.ref === "string" && STATES.includes(String(item.state)) && (item.number === void 0 || Number.isInteger(item.number)) && (item.input === void 0 || typeof item.input === "string") && (item.mistake === void 0 || Number.isInteger(item.mistake)) && (item.missed === void 0 || Array.isArray(item.missed) && item.missed.every((point) => Number.isInteger(point))));
1569
+ }
1570
+ /** The words of each band, as the result shows them. */
1571
+ const BAND_MESSAGES = {
1572
+ known: "assessmentKnown",
1573
+ partly: "assessmentPartly",
1574
+ read: "assessmentRead",
1575
+ unasked: "assessmentUnasked"
1576
+ };
1577
+ /**
1578
+ * The band of `right` and `partly` of `total`: all right of two or more, at least half, less; not asked for none. A
1579
+ * partly right task counts half. One task alone is too little to say a page is known: right, it is "partly".
1580
+ */
1581
+ function bandOf(right, total, partly = 0) {
1582
+ if (total === 0) return "unasked";
1583
+ if (right === total && total >= 2) return "known";
1584
+ return right * 2 + partly >= total ? "partly" : "read";
1585
+ }
1586
+ /**
1587
+ * The result of each content page in `pages` (each <chapter>/<page>/, in order), from the results of a run and the page
1588
+ * each exercise is from (`pageOf`, by key); an exercise the content no longer has does not count.
1589
+ */
1590
+ function pageResults(pages, pageOf, results) {
1591
+ return pages.map((page) => {
1592
+ const asked = results.filter((result) => pageOf[result.key] === page);
1593
+ const right = asked.filter((result) => result.state === "richtig").length;
1594
+ const partly = asked.filter((result) => result.state === "teilweise").length;
1595
+ return {
1596
+ page,
1597
+ right,
1598
+ partly,
1599
+ total: asked.length,
1600
+ band: bandOf(right, asked.length, partly)
1601
+ };
1602
+ });
1603
+ }
1604
+ /** The value of one exercise in the query of the tutor page: 17-falsch-F1, richtig, 3-offen, teilweise-P2P3. */
1605
+ function resultValue(result) {
1606
+ return [
1607
+ ...result.number === void 0 ? [] : [String(result.number)],
1608
+ result.state,
1609
+ ...result.state === "falsch" && result.mistake !== void 0 ? [`F${result.mistake}`] : [],
1610
+ ...result.missed !== void 0 && result.missed.length > 0 ? [result.missed.map((point) => `P${point}`).join("")] : []
1611
+ ].join("-");
1612
+ }
1613
+ /** The query of the tutor page with a run's results: ?a1=17-falsch-F1&a2=richtig. */
1614
+ function resultQuery(results) {
1615
+ const query = new URLSearchParams();
1616
+ for (const result of results) query.set(result.key, resultValue(result));
1617
+ const text = query.toString();
1618
+ return text === "" ? "" : `?${text}`;
1619
+ }
1620
+ const oneLine = (text) => text.replace(/\s+/g, " ").trim();
1621
+ /** The words of each state in a line of the prompt. */
1622
+ const STATE_MESSAGES = {
1623
+ richtig: "assessmentRight",
1624
+ teilweise: "assessmentPartlyRight",
1625
+ falsch: "assessmentWrong",
1626
+ offen: "assessmentOpen"
1627
+ };
1628
+ /**
1629
+ * The prompt that takes a run to a tutor, in `lang`, in the learner's voice: the tutor page with the results in its
1630
+ * address first, then the result of each content page as the learner saw it (its title in `pages`), so that the tutor
1631
+ * works nothing out, then one line per exercise (its page, the exercise, its task number, how it went, the input of a
1632
+ * wrong one). The solution is never in it. `back`: for a chat that runs already (via=tutor), the lines with the tutor
1633
+ * page last, which "Zurück zum Tutor" copies.
1634
+ */
1635
+ function assessmentPrompt(tutor, items, results, lang, pages = [], opening = "ask", links = []) {
1636
+ const bands = pages.map((page) => page.band === "unasked" ? `- ${message(lang, "assessmentPageUnasked", { page: oneLine(page.title) })}` : `- ${message(lang, page.partly > 0 ? "assessmentPageLinePartly" : "assessmentPageLine", {
1637
+ page: oneLine(page.title),
1638
+ band: message(lang, BAND_MESSAGES[page.band]),
1639
+ right: String(page.right),
1640
+ partly: String(page.partly),
1641
+ total: String(page.total)
1642
+ })}`);
1643
+ const lines = results.map((result) => {
1644
+ const item = items.find((candidate) => candidate.key === result.key);
1645
+ const state = message(lang, STATE_MESSAGES[result.state]);
1646
+ const missed = result.missed !== void 0 && result.missed.length > 0 ? ` ${message(lang, "assessmentMissed", { points: result.missed.map((point) => `P${point}`).join(", ") })}` : "";
1647
+ const input = (result.state === "falsch" || result.state === "teilweise") && result.input !== void 0 ? ` ${message(lang, "assessmentInput", { input: oneLine(result.input) })}` : "";
1648
+ return `- ${message(lang, "assessmentLine", {
1649
+ key: result.key.toUpperCase(),
1650
+ page: oneLine(item?.page ?? ""),
1651
+ exercise: item?.exercise ?? "",
1652
+ number: result.number === void 0 ? "" : message(lang, "resultNumber", { n: String(result.number) }),
1653
+ state
1654
+ })}${missed}${input}`;
1655
+ });
1656
+ if (opening === "back") return [
1657
+ message(lang, "assessmentBackLine"),
1658
+ ...bands,
1659
+ message(lang, "assessmentTasks"),
1660
+ ...lines,
1661
+ ...links,
1662
+ `${message(lang, "resultTutor")} ${tutor}`
1663
+ ].join("\n");
1664
+ return [
1665
+ message(lang, "assessmentAsk", { url: tutor }),
1666
+ ...bands,
1667
+ ...bands.length === 0 ? [] : [message(lang, "assessmentTasks")],
1668
+ ...lines,
1669
+ ...links
1670
+ ].join("\n");
1671
+ }
1672
+ /**
1673
+ * The full address of every page and every self-assessment of the app, chapter by chapter, each with via=tutor, from
1674
+ * `root`, the address of the language tree as the browser has it: a tutor takes these and builds no address itself.
1675
+ */
1676
+ function linkLines(root, chapters, whole, lang) {
1677
+ const at = (local) => {
1678
+ const url = new URL(local, root);
1679
+ url.searchParams.set("via", "tutor");
1680
+ return url.href;
1681
+ };
1682
+ return [
1683
+ message(lang, "assessmentLinks"),
1684
+ ...chapters.flatMap((chapter) => [chapter.assessment === void 0 ? message(lang, "assessmentLinkChapterNone", { chapter: chapter.title }) : message(lang, "assessmentLinkChapter", {
1685
+ chapter: chapter.title,
1686
+ url: at(chapter.assessment)
1687
+ }), ...chapter.pages.map((page) => `- ${message(lang, "assessmentLinkPage", {
1688
+ page: page.title,
1689
+ url: at(page.local)
1690
+ })}`)]),
1691
+ ...chapters.length > 1 ? [message(lang, "assessmentLinkWhole", { url: at(whole) })] : []
1692
+ ];
1693
+ }
1694
+ /** The title of a content page in `lang`, on one line. */
1695
+ const titleOf = (page, lang) => oneLine(inLanguage(page.title, lang));
1696
+ export { parse as $, answerFor as A, label as B, watchAddress as C, generate as D, compiled as E, questionFor as F, taskText as G, placeholdersIn as H, taskKey as I, tokenize as J, templateProblems as K, answerLabel as L, exampleGiving as M, mistakeOf as N, holds as O, pairValues as P, evaluate as Q, fill as R, videoLine as S, StarvedError as T, plainText as U, placeholderValue as V, stepsOf as W, kindOf as X, unusedVariables as Y, ExpressionError as Z, termId as _, assessments as a, renderVideo as b, isStoredResult as c, parseRef as d, same as et, resultQuery as f, isExercise as g, allBlocks as h, assessmentPrompt as i, diagnose as j, values as k, linkLines as l, titleOf as m, BAND_MESSAGES as n, Fraction as nt, drawsTasks as o, stepHint as p, templatesIn as q, assessmentOf as r, exerciseOf as s, ASSESSMENT as t, variables as tt, pageResults as u, timelineEvents as v, message as w, startVideos as x, embedAddress as y, hintOf as z };