@lernapps/app-templates 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +195 -2
  3. package/dist/appname-Bh23cUuz.mjs +12 -0
  4. package/dist/assessment-By2GGTJC.mjs +1696 -0
  5. package/dist/block-BK_ohsH1.mjs +82 -0
  6. package/dist/blocks-BmRrD7UY.mjs +149 -0
  7. package/dist/blocks.mjs +4 -0
  8. package/dist/check-BkEl-m-F.mjs +254 -0
  9. package/dist/content-cYBPYg_J.mjs +140 -0
  10. package/dist/contract-PONpxJtd.mjs +52 -0
  11. package/dist/e2e-BiMI-pCL.mjs +89 -0
  12. package/dist/engine-BqF2Vffn.mjs +136 -0
  13. package/dist/explainer-e2e.mjs +1297 -0
  14. package/dist/explainer-islands.mjs +197 -0
  15. package/dist/explainer-plugin.mjs +2131 -0
  16. package/dist/explainer.mjs +1246 -0
  17. package/dist/html-mffkHGHh.mjs +89 -0
  18. package/dist/image-CspJVfyQ.mjs +21 -0
  19. package/dist/interactive-e2e.mjs +140 -0
  20. package/dist/interactive-plugin.mjs +242 -0
  21. package/dist/interactive.mjs +75 -0
  22. package/dist/load-C-0hUaSC.mjs +16947 -0
  23. package/dist/needs-BzEahJ6X.mjs +97 -0
  24. package/dist/pages-9RaaomoO.mjs +81 -0
  25. package/dist/questions-e2e.mjs +1 -0
  26. package/dist/questions-engine.mjs +3 -0
  27. package/dist/questions.mjs +6 -0
  28. package/dist/quiz-BKc8JYtC.mjs +70 -0
  29. package/dist/quiz-e2e.mjs +174 -0
  30. package/dist/quiz-plugin.mjs +266 -0
  31. package/dist/quiz.mjs +246 -0
  32. package/dist/render-BJey0bwl.mjs +540 -0
  33. package/dist/render-BL6fV_NI.mjs +507 -0
  34. package/dist/render-DpRO_RKm.mjs +146 -0
  35. package/dist/result-_eQcamoz.mjs +180 -0
  36. package/dist/tasks-DADUYy52.mjs +74 -0
  37. package/dist/validate-COaZso7u.mjs +1072 -0
  38. package/dist/validate-CxtdeQ5S.mjs +146 -0
  39. package/dist/vue-DO1m6q1R.mjs +40 -0
  40. package/dist/vue-code-BkJjW_I_.mjs +320 -0
  41. package/docs/arc42/01-introduction-and-goals.arc42.md +82 -0
  42. package/docs/arc42/02-constraints.arc42.md +228 -0
  43. package/docs/arc42/03-system-context.arc42.md +118 -0
  44. package/docs/arc42/04-solution-strategy.arc42.md +47 -0
  45. package/docs/arc42/05-building-blocks.arc42.md +307 -0
  46. package/docs/arc42/06-runtime-view.arc42.md +98 -0
  47. package/docs/arc42/07-deployment-view.arc42.md +160 -0
  48. package/docs/arc42/08-concepts.arc42.md +123 -0
  49. package/docs/arc42/09-decisions.arc42.md +246 -0
  50. package/docs/arc42/10-quality-requirements.arc42.md +155 -0
  51. package/docs/arc42/11-risks.arc42.md +113 -0
  52. package/docs/arc42/12-glossary.arc42.md +162 -0
  53. package/docs/arc42/architecture-evidence.md +395 -0
  54. package/explainer/.arc42ignore +3 -0
  55. package/explainer/README.md +54 -0
  56. package/explainer/_gitignore +5 -0
  57. package/explainer/docs/arc42/03-explainer-system-context.arc42.md +35 -0
  58. package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +659 -0
  59. package/explainer/docs/arc42/06-explainer-runtime-view.arc42.md +105 -0
  60. package/explainer/docs/arc42/07-explainer-deployment-view.arc42.md +34 -0
  61. package/explainer/docs/arc42/08-explainer-concepts.arc42.md +17 -0
  62. package/explainer/docs/arc42/09-explainer-decisions.arc42.md +443 -0
  63. package/explainer/docs/arc42/10-explainer-quality-requirements.arc42.md +54 -0
  64. package/explainer/docs/arc42/11-explainer-risks.arc42.md +17 -0
  65. package/explainer/docs/arc42/12-explainer-glossary.arc42.md +185 -0
  66. package/explainer/e2e/explainer.e2e.ts +3 -0
  67. package/explainer/index.html +11 -0
  68. package/explainer/package.json +18 -0
  69. package/explainer/playwright.config.ts +1 -0
  70. package/explainer/src/app.yaml +20 -0
  71. package/explainer/src/components/ProzentFeld.test.ts +30 -0
  72. package/explainer/src/components/ProzentFeld.ts +121 -0
  73. package/explainer/src/components/ProzentFeld.vue +84 -0
  74. package/explainer/src/images/hunderterfeld-klammern.svg +105 -0
  75. package/explainer/src/main.ts +4 -0
  76. package/explainer/src/prozent/chapter.yaml +18 -0
  77. package/explainer/src/prozent/grundwert.yaml +115 -0
  78. package/explainer/src/prozent/prozentsatz.yaml +99 -0
  79. package/explainer/src/prozent/prozentwert.yaml +119 -0
  80. package/explainer/tsconfig.json +1 -0
  81. package/explainer/vite.config.ts +4 -0
  82. package/interactive/.arc42ignore +5 -0
  83. package/interactive/README.md +28 -0
  84. package/interactive/_gitignore +4 -0
  85. package/interactive/docs/arc42/05-interactive-building-blocks.arc42.md +518 -0
  86. package/interactive/docs/arc42/06-interactive-runtime-view.arc42.md +58 -0
  87. package/interactive/docs/arc42/07-interactive-deployment-view.arc42.md +35 -0
  88. package/interactive/docs/arc42/08-interactive-concepts.arc42.md +52 -0
  89. package/interactive/docs/arc42/09-interactive-decisions.arc42.md +130 -0
  90. package/interactive/docs/arc42/10-interactive-quality-requirements.arc42.md +45 -0
  91. package/interactive/docs/arc42/11-interactive-risks.arc42.md +20 -0
  92. package/interactive/docs/arc42/12-interactive-glossary.arc42.md +80 -0
  93. package/interactive/e2e/interactive.e2e.ts +3 -0
  94. package/interactive/index.html +11 -0
  95. package/interactive/package.json +18 -0
  96. package/interactive/playwright.config.ts +1 -0
  97. package/interactive/src/app.yaml +28 -0
  98. package/interactive/src/core/index.ts +101 -0
  99. package/interactive/src/data/lessons.json +4 -0
  100. package/interactive/src/env.d.ts +6 -0
  101. package/interactive/src/main.ts +6 -0
  102. package/interactive/src/messages/de.json +12 -0
  103. package/interactive/src/playthroughs/grundreihe.yaml +33 -0
  104. package/interactive/src/playthroughs/obere-reihe.yaml +14 -0
  105. package/interactive/src/ui/App.vue +85 -0
  106. package/interactive/tsconfig.json +1 -0
  107. package/interactive/vite.config.ts +4 -0
  108. package/package.json +142 -4
  109. package/quiz/README.md +31 -0
  110. package/quiz/_gitignore +5 -0
  111. package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +283 -0
  112. package/quiz/docs/arc42/06-quiz-runtime-view.arc42.md +21 -0
  113. package/quiz/docs/arc42/07-quiz-deployment-view.arc42.md +33 -0
  114. package/quiz/docs/arc42/09-quiz-decisions.arc42.md +36 -0
  115. package/quiz/docs/arc42/10-quiz-quality-requirements.arc42.md +53 -0
  116. package/quiz/docs/arc42/11-quiz-risks.arc42.md +19 -0
  117. package/quiz/docs/arc42/12-quiz-glossary.arc42.md +27 -0
  118. package/quiz/e2e/quiz.e2e.ts +3 -0
  119. package/quiz/index.html +11 -0
  120. package/quiz/package.json +18 -0
  121. package/quiz/playwright.config.ts +1 -0
  122. package/quiz/src/images/berge-ordnen.jpg +0 -0
  123. package/quiz/src/images/everest-hoehe.jpg +0 -0
  124. package/quiz/src/images/galapagos.jpg +0 -0
  125. package/quiz/src/images/grand-canyon.jpg +0 -0
  126. package/quiz/src/images/totes-meer.jpg +0 -0
  127. package/quiz/src/images/uluru.jpg +0 -0
  128. package/quiz/src/images/wasserfaelle-laender.jpg +0 -0
  129. package/quiz/src/main.ts +4 -0
  130. package/quiz/src/quiz.json +312 -0
  131. package/quiz/tsconfig.json +1 -0
  132. package/quiz/vite.config.ts +4 -0
  133. package/runtime/explainer/assessment-client.ts +478 -0
  134. package/runtime/explainer/assessment-page.ts +446 -0
  135. package/runtime/explainer/assessment.ts +393 -0
  136. package/runtime/explainer/check-page.ts +520 -0
  137. package/runtime/explainer/components/exercise.ts +409 -0
  138. package/runtime/explainer/components/props.ts +230 -0
  139. package/runtime/explainer/components/read.ts +428 -0
  140. package/runtime/explainer/content.ts +277 -0
  141. package/runtime/explainer/e2e.ts +1474 -0
  142. package/runtime/explainer/exercises.ts +324 -0
  143. package/runtime/explainer/explainer.css +1283 -0
  144. package/runtime/explainer/explainer.ts +838 -0
  145. package/runtime/explainer/explainer.v1.schema.json +1165 -0
  146. package/runtime/explainer/expression.ts +407 -0
  147. package/runtime/explainer/fraction.ts +107 -0
  148. package/runtime/explainer/generated/comparator.ts +31 -0
  149. package/runtime/explainer/generated/question.ts +165 -0
  150. package/runtime/explainer/generated/soundness.ts +280 -0
  151. package/runtime/explainer/generated/texts.ts +257 -0
  152. package/runtime/explainer/generated/types.ts +103 -0
  153. package/runtime/explainer/generated/values.ts +136 -0
  154. package/runtime/explainer/islands.ts +281 -0
  155. package/runtime/explainer/load.ts +457 -0
  156. package/runtime/explainer/messages/de.json +200 -0
  157. package/runtime/explainer/messages/en.json +200 -0
  158. package/runtime/explainer/messages.ts +13 -0
  159. package/runtime/explainer/needs.ts +122 -0
  160. package/runtime/explainer/open-task.ts +235 -0
  161. package/runtime/explainer/outline.ts +299 -0
  162. package/runtime/explainer/plugin.ts +488 -0
  163. package/runtime/explainer/position.ts +62 -0
  164. package/runtime/explainer/prompt.ts +75 -0
  165. package/runtime/explainer/render.ts +686 -0
  166. package/runtime/explainer/result.ts +238 -0
  167. package/runtime/explainer/routes.ts +119 -0
  168. package/runtime/explainer/schema.ts +140 -0
  169. package/runtime/explainer/scope.ts +87 -0
  170. package/runtime/explainer/stuck.ts +57 -0
  171. package/runtime/explainer/tasks.ts +109 -0
  172. package/runtime/explainer/tutor.ts +469 -0
  173. package/runtime/explainer/tutorpage.ts +669 -0
  174. package/runtime/explainer/video.ts +123 -0
  175. package/runtime/interactive/contract.ts +121 -0
  176. package/runtime/interactive/e2e.ts +190 -0
  177. package/runtime/interactive/interactive.css +82 -0
  178. package/runtime/interactive/interactive.ts +115 -0
  179. package/runtime/interactive/interactive.v1.schema.json +146 -0
  180. package/runtime/interactive/load.ts +188 -0
  181. package/runtime/interactive/messages/de.json +14 -0
  182. package/runtime/interactive/messages/en.json +14 -0
  183. package/runtime/interactive/messages.ts +11 -0
  184. package/runtime/interactive/playthrough.v1.schema.json +143 -0
  185. package/runtime/interactive/plugin.ts +266 -0
  186. package/runtime/interactive/render.ts +126 -0
  187. package/runtime/quiz/e2e.ts +233 -0
  188. package/runtime/quiz/engine.ts +242 -0
  189. package/runtime/quiz/messages/de.json +23 -0
  190. package/runtime/quiz/messages/en.json +23 -0
  191. package/runtime/quiz/plugin.ts +150 -0
  192. package/runtime/quiz/quiz.css +154 -0
  193. package/runtime/quiz/quiz.ts +56 -0
  194. package/runtime/quiz/quiz.v1.schema.json +72 -0
  195. package/runtime/quiz/render.ts +173 -0
  196. package/runtime/quiz/validate.ts +108 -0
  197. package/runtime/shared/appname.ts +16 -0
  198. package/runtime/shared/blocks/block.ts +162 -0
  199. package/runtime/shared/blocks/blocks.css +190 -0
  200. package/runtime/shared/blocks/blocks.v1.schema.json +432 -0
  201. package/runtime/shared/blocks/code.ts +26 -0
  202. package/runtime/shared/blocks/figures.ts +49 -0
  203. package/runtime/shared/blocks/image.ts +25 -0
  204. package/runtime/shared/blocks/index.ts +13 -0
  205. package/runtime/shared/blocks/messages/de.json +4 -0
  206. package/runtime/shared/blocks/messages/en.json +4 -0
  207. package/runtime/shared/blocks/render.ts +134 -0
  208. package/runtime/shared/blocks/validate.ts +165 -0
  209. package/runtime/shared/content.ts +197 -0
  210. package/runtime/shared/examples.ts +20 -0
  211. package/runtime/shared/html.ts +10 -0
  212. package/runtime/shared/languages.ts +94 -0
  213. package/runtime/shared/page.css +101 -0
  214. package/runtime/shared/pages.ts +114 -0
  215. package/runtime/shared/questions/e2e.ts +115 -0
  216. package/runtime/shared/questions/engine.ts +209 -0
  217. package/runtime/shared/questions/index.ts +39 -0
  218. package/runtime/shared/questions/messages/de.json +26 -0
  219. package/runtime/shared/questions/messages/en.json +26 -0
  220. package/runtime/shared/questions/question.ts +430 -0
  221. package/runtime/shared/questions/question.v1.schema.json +342 -0
  222. package/runtime/shared/questions/questions.css +192 -0
  223. package/runtime/shared/questions/render.ts +247 -0
  224. package/runtime/shared/questions/validate.ts +518 -0
  225. package/runtime/shared/schema.ts +44 -0
  226. package/runtime/shared/text/check.ts +68 -0
  227. package/runtime/shared/text/formula.ts +69 -0
  228. package/runtime/shared/text/markdown.ts +185 -0
  229. package/runtime/shared/text/text.v1.schema.json +34 -0
  230. package/runtime/shared/vue-code.ts +395 -0
  231. package/runtime/shared/vue.ts +38 -0
@@ -0,0 +1,109 @@
1
+ // The task numbers of an explainer's seeded exercises (generated ones and exercise components), shared by the client
2
+ // module, the islands and the build. The build walks the seeds 1, 2, 3, … through an exercise's generator and keeps
3
+ // each seed whose task differs from every task before it (distinctSeeds), up to MAX_TASKS; the kept seeds, in order,
4
+ // are the tasks "Aufgabe Nr. 1" to "Aufgabe Nr. N". The number is the one identifier a learner, a tutor and the
5
+ // address share: ?seed=<n>#<exercise> opens task number n (the build ships each exercise's table of seeds, and the page
6
+ // draws the task from table[n - 1]); "Neue Aufgabe" moves to the next number, from N back to 1; a number outside 1 to N
7
+ // wraps around into it (taskNumber), so every address opens a task the tutor page lists (tutorpage.ts).
8
+
9
+ /** The most tasks an exercise numbers. */
10
+ export const MAX_TASKS = 67;
11
+ /** How many seeds the build tries at most to find MAX_TASKS distinct tasks. */
12
+ export const SEED_TRIES = MAX_TASKS * 20;
13
+
14
+ /**
15
+ * The seeds of the distinct tasks, in order: seed 1, 2, 3, … kept when `key(seed)` (what the learner sees of its task,
16
+ * language-independent) differs from every kept one's, until `max` are kept or `tries` seeds are tried; a seed whose
17
+ * key is undefined (its task cannot be drawn) is skipped.
18
+ */
19
+ export function distinctSeeds(
20
+ key: (seed: number) => string | undefined,
21
+ max = MAX_TASKS,
22
+ tries = SEED_TRIES,
23
+ ): number[] {
24
+ const seen = new Set<string>();
25
+ const seeds: number[] = [];
26
+ for (let seed = 1; seed <= tries && seeds.length < max; seed++) {
27
+ const found = key(seed);
28
+ if (found === undefined || seen.has(found)) continue;
29
+ seen.add(found);
30
+ seeds.push(seed);
31
+ }
32
+ return seeds;
33
+ }
34
+
35
+ /** The task number `n` within 1 to `count`: a number outside wraps around into it (68 of 67 is 1). */
36
+ export const taskNumber = (n: number, count: number): number => ((((Math.trunc(n) - 1) % count) + count) % count) + 1;
37
+
38
+ /** The task number after `n`, for "Neue Aufgabe": the next one, from `count` back to 1. */
39
+ export const nextTaskNumber = (n: number, count: number): number => (n % count) + 1;
40
+
41
+ /** A task number to start with when the address names none. */
42
+ export const randomTaskNumber = (count: number): number => 1 + Math.floor(Math.random() * count);
43
+
44
+ /**
45
+ * Address parameters that are no exercise's: the page's (seed, via) and those of its tutor page, whose address holds
46
+ * the page's task numbers beside them (uebung, abschnitt; position.ts). An exercise id must not be one of them (the
47
+ * build checks).
48
+ */
49
+ export const RESERVED_PARAMS: readonly string[] = ["seed", "via", "uebung", "abschnitt"];
50
+
51
+ /**
52
+ * The task number the address `href` gives the exercise `id`: the parameter named by its id (?<id>=<n>, one per
53
+ * exercise, so that the address holds the task of every exercise of the page), or, as before, ?seed=<n>#<id>.
54
+ */
55
+ export function numberFromAddress(href: string, id: string): number | undefined {
56
+ const url = new URL(href);
57
+ const asNumber = (text: string | null) => {
58
+ const n = Number(text);
59
+ return text !== null && Number.isInteger(n) && n > 0 ? n : undefined;
60
+ };
61
+ const own = asNumber(url.searchParams.get(id));
62
+ if (own !== undefined) return own;
63
+ return decodeURIComponent(url.hash.slice(1)) === id ? asNumber(url.searchParams.get("seed")) : undefined;
64
+ }
65
+
66
+ /**
67
+ * The address `href` with the task number `n` of the exercise `id`: ?…&<id>=<n>, its other parameters and its anchor
68
+ * kept. A ?seed=<n> that named this exercise's task (it is the anchor) becomes the exercise's own parameter.
69
+ */
70
+ export function addressOf(href: string, id: string, n: number): string {
71
+ const url = new URL(href);
72
+ if (decodeURIComponent(url.hash.slice(1)) === id) url.searchParams.delete("seed");
73
+ url.searchParams.set(id, String(n));
74
+ return url.href;
75
+ }
76
+
77
+ /** How many worked tasks a page shows without JavaScript: the task numbers 1 to 3. */
78
+ export const WORKED_TASKS = 3;
79
+
80
+ /** "Neue Aufgabe" as a symbol beside the task number: a circular arrow; the button's label is its name. */
81
+ export const NEW_TASK_ICON =
82
+ '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M21 12a9 9 0 1 1-2.6-6.4"/><path d="M21 3v6h-6"/></svg>';
83
+
84
+ /** The event an exercise sends on its element once it shows a task: the tutor's help button takes its place again. */
85
+ export const SHOWN_EVENT = "explainer-shown";
86
+
87
+ /**
88
+ * The events between a self-assessment (assessment-client.ts) and the islands (islands.ts): the self-assessment put
89
+ * its exercises into the page (on the document); it gives an exercise component the task of `seed` (on its element);
90
+ * the component's answer changed, as { correct, partly?, answer?, message?, mistake?, missed? }, with mistake the
91
+ * index of the typical mistake it makes from 0, and missed the indexes of the points of a good answer it lacks, each
92
+ * from 0; or null when it has none: emptied, incomplete, or one its logic cannot read (on its element, bubbling).
93
+ */
94
+ export const ASSESSMENT_UNPACKED_EVENT = "explainer-assessment-unpacked";
95
+ export const ASSESSMENT_TASK_EVENT = "explainer-assessment-task";
96
+ export const ASSESSMENT_ANSWER_EVENT = "explainer-assessment-answer";
97
+
98
+ /** What an exercise component of a self-assessment says of the answer it has now (ASSESSMENT_ANSWER_EVENT). */
99
+ export interface AssessmentAnswer {
100
+ correct: boolean;
101
+ /** Not right, but partly: some, not all, points of a good answer. */
102
+ partly?: boolean;
103
+ /** The answer as text. */
104
+ answer?: string;
105
+ message?: string;
106
+ mistake?: number;
107
+ /** The points of a good answer it lacks, each from 0, for a partly right or wrong answer. */
108
+ missed?: number[];
109
+ }
@@ -0,0 +1,469 @@
1
+ // What a KI tutor reads of an explainer's content, shared by the tutor page of each content page (tutorpage.ts), the
2
+ // one source of what the tutor gets: the page's content as Markdown lines (contentLines), each exercise by its id,
3
+ // type and what the learner does, answers and is told (exercisePart), the words of both, and the check of their
4
+ // links against the pages and anchors the build writes (linkProblems). A chat assistant that fetches a URL runs no
5
+ // JavaScript, and a static host serves the same file for any query string: so all of it is static text. The app
6
+ // itself collects nothing and sends nothing: the tutor page only helps a tutor guide.
7
+ import { markerOf } from "../shared/blocks/block.ts";
8
+ import { questionIn, type Question } from "../shared/questions/question.ts";
9
+ import { answerText } from "../shared/questions/render.ts";
10
+ import {
11
+ inLanguage,
12
+ timelineEvents,
13
+ type Block,
14
+ type BlockOf,
15
+ type ComponentDescription,
16
+ type Page,
17
+ type Site,
18
+ type Text,
19
+ type Descriptions,
20
+ type OpenTask,
21
+ } from "./content.ts";
22
+ import { plainText } from "./generated/texts.ts";
23
+ import { kindOf } from "./generated/comparator.ts";
24
+ import { type Generated } from "./generated/types.ts";
25
+ import { allBlocks } from "./routes.ts";
26
+ import { videoLine } from "./video.ts";
27
+
28
+ interface Words {
29
+ task: string;
30
+ answerFormat: string;
31
+ solution: string;
32
+ explanation: string;
33
+ hint: string;
34
+ mistakes: string;
35
+ /** The typical mistakes of an exercise component: descriptions for the tutor, not the app's messages. */
36
+ componentMistakes: string;
37
+ options: string;
38
+ right: string;
39
+ wrong: string;
40
+ component: string;
41
+ does: string;
42
+ correct: string;
43
+ example: string;
44
+ note: string;
45
+ figure: string;
46
+ caption: string;
47
+ labels: string;
48
+ interactive: string;
49
+ video: string;
50
+ exerciseHere: string;
51
+ /** An open task: its points, the model answer, the ways to answer, how to help. */
52
+ points: string;
53
+ pointContent: string;
54
+ pointForm: string;
55
+ source: string;
56
+ modelAnswer: string;
57
+ answerWays: Record<"text" | "photo" | "both", string>;
58
+ openGuide: string;
59
+ formats: Record<string, string>;
60
+ }
61
+
62
+ const WORDS: Readonly<Record<string, Words>> = {
63
+ de: {
64
+ task: "Aufgabe",
65
+ answerFormat: "Antwortformat",
66
+ solution: "Lösung",
67
+ explanation: "Erklärung",
68
+ hint: "Tipp der App",
69
+ mistakes: "Typische Fehler und was die App sagt",
70
+ componentMistakes: "Typische Fehler (die Meldung der App passt zu einem davon)",
71
+ options: "Antworten zur Wahl (✓ richtig, ✗ falsch, mit der Meldung der App)",
72
+ right: "✓",
73
+ wrong: "✗",
74
+ component: "interaktive Übung",
75
+ does: "Was zu tun ist",
76
+ correct: "Was als richtig zählt",
77
+ example: "Beispiel",
78
+ note: "Merke",
79
+ figure: "Bild",
80
+ caption: "Bildunterschrift",
81
+ labels: "Beschriftungen",
82
+ interactive: "Interaktiv",
83
+ video: "Video",
84
+ exerciseHere: "Übung `{id}` (unten)",
85
+ points: "Eine gute Antwort enthält (P1, P2 … wie die App sie nennt)",
86
+ pointContent: "Inhalt",
87
+ pointForm: "Form",
88
+ source: "Quelle",
89
+ modelAnswer: "Musterantwort",
90
+ answerWays: {
91
+ text: "in eigenen Worten eintippen",
92
+ photo: "in eigenen Worten auf Papier schreiben und das Blatt fotografieren",
93
+ both: "in eigenen Worten eintippen oder auf Papier schreiben und das Blatt fotografieren",
94
+ },
95
+ openGuide:
96
+ "So hilfst du: Die App zeigt die Punkte erst nach der Antwort; das Kind hakt selbst ab, welche seine Antwort enthält, und „es fehlen P2, P4“ nennt die anderen. Fehlt dir die Antwort, bitte das Kind, sie einzufügen oder das Foto seines Blatts anzuhängen. Prüf Punkt für Punkt, auch die abgehakten, und sag zuerst, was gelungen ist. Zu einem fehlenden Punkt stell eine Frage, die dorthin führt, und nenn seine Quelle. Verrate die Musterantwort nicht, bevor das Kind es noch einmal versucht hat. Zu einem Punkt der Form zeig an einem Satz des Kindes, wie es besser geht.",
97
+ formats: {
98
+ "single-choice": "eine Antwort wählen",
99
+ "multiple-choice": "alle richtigen Antworten wählen",
100
+ "true-false": "„Stimmt“ oder „Stimmt nicht“ wählen",
101
+ number: "eine Zahl eintippen",
102
+ ordering: "die Einträge in die richtige Reihenfolge bringen (die App zeigt sie gemischt)",
103
+ matching: "jedem Eintrag links den passenden rechts zuordnen",
104
+ choice: "eine Antwort wählen",
105
+ set: "alle richtigen Antworten wählen",
106
+ order: "die Einträge in die richtige Reihenfolge bringen (die App zeigt sie gemischt)",
107
+ pairs: "jedem Eintrag links den passenden rechts zuordnen",
108
+ },
109
+ },
110
+ en: {
111
+ task: "Task",
112
+ answerFormat: "Answer format",
113
+ solution: "Solution",
114
+ explanation: "Explanation",
115
+ hint: "The app's hint",
116
+ mistakes: "Typical mistakes and what the app says",
117
+ componentMistakes: "Typical mistakes (the app's message fits one of them)",
118
+ options: "Choices (✓ right, ✗ wrong, with the app's message)",
119
+ right: "✓",
120
+ wrong: "✗",
121
+ component: "interactive exercise",
122
+ does: "What to do",
123
+ correct: "What counts as right",
124
+ example: "Example",
125
+ note: "Remember",
126
+ figure: "Picture",
127
+ caption: "Caption",
128
+ labels: "Labels",
129
+ interactive: "Interactive",
130
+ video: "Video",
131
+ exerciseHere: "Exercise `{id}` (below)",
132
+ points: "A good answer has (P1, P2 … as the app names them)",
133
+ pointContent: "content",
134
+ pointForm: "form",
135
+ source: "Source",
136
+ modelAnswer: "Model answer",
137
+ answerWays: {
138
+ text: "type it in their own words",
139
+ photo: "write it in their own words on paper and take a photo of the sheet",
140
+ both: "type it in their own words, or write it on paper and take a photo of the sheet",
141
+ },
142
+ openGuide:
143
+ "How to help: the app shows the points only after the answer; the child ticks which ones the answer has, and “missing P2, P4” names the others. If you lack the answer, ask the child to paste it or to attach the photo of the sheet. Check point by point, the ticked ones too, and say first what went well. For a missing point, ask a question that leads to it and name its source. Do not give away the model answer before the child has tried again. For a point of form, show with one of the child's sentences how it gets better.",
144
+ formats: {
145
+ "single-choice": "choose one answer",
146
+ "multiple-choice": "choose all right answers",
147
+ "true-false": "choose “true” or “false”",
148
+ number: "type a number",
149
+ ordering: "put the items in the right order (the app shows them shuffled)",
150
+ matching: "match each item on the left with one on the right",
151
+ choice: "choose one answer",
152
+ set: "choose all right answers",
153
+ order: "put the items in the right order (the app shows them shuffled)",
154
+ pairs: "match each item on the left with one on the right",
155
+ },
156
+ },
157
+ };
158
+
159
+ const wordsIn = (lang: string): Words => WORDS[lang] ?? (WORDS["de"] as Words);
160
+
161
+ /** The words of what a tutor reads in `lang`, which the tutor page shares (tutorpage.ts). */
162
+ export const tutorWords = (lang: string): Readonly<Words> => wordsIn(lang);
163
+ export type { Words as TutorWords };
164
+
165
+ /** A text on one line, as a list item holds it. */
166
+ const line = (text: string) => text.replace(/\s+/g, " ").trim();
167
+
168
+ /** The folder of a page's tutor page, in the page's folder: <chapter>/<page>/tutor/ (tutorpage.ts). */
169
+ export const TUTOR_PAGE = "tutor";
170
+
171
+ /** The address of a page's tutor page within its language tree. */
172
+ export const tutorPageOf = (page: Pick<Page, "chapter" | "id">): string => `${page.chapter}/${page.id}/${TUTOR_PAGE}/`;
173
+
174
+ /** The answer format of a question of the question types, in `lang`. */
175
+ function questionFormat(question: Question, words: Words): string {
176
+ const format = words.formats[question.type] ?? "";
177
+ return question.type === "number" && question.unit !== undefined ? `${format} (${question.unit})` : format;
178
+ }
179
+
180
+ /** A question of the question types, in `lang`: its text, format, options, solution, explanation, hint, mistakes. */
181
+ function questionPart(question: Question, lang: string, words: Words): string[] {
182
+ const lines = [
183
+ `- ${words.task}: ${line(question.text)}`,
184
+ `- ${words.answerFormat}: ${questionFormat(question, words)}`,
185
+ ];
186
+ if (question.type === "single-choice" || question.type === "multiple-choice") {
187
+ lines.push(`- ${words.options}:`);
188
+ for (const option of question.options) {
189
+ const info = option.info.trim() === "" ? "" : ` – ${line(option.info)}`;
190
+ lines.push(` - ${option.correct ? words.right : words.wrong} ${line(option.text)}${info}`);
191
+ }
192
+ }
193
+ lines.push(`- ${words.solution}: ${line(plainText(answerText(question, lang)))}`);
194
+ if (question.explanation.trim() !== "") lines.push(`- ${words.explanation}: ${line(question.explanation)}`);
195
+ if (question.hint !== undefined) lines.push(`- ${words.hint}: ${line(question.hint)}`);
196
+ if (question.type === "number" && (question.mistakes ?? []).length > 0) {
197
+ lines.push(`- ${words.mistakes}:`);
198
+ for (const mistake of question.mistakes ?? []) lines.push(` - ${mistake.answer}: ${line(mistake.message)}`);
199
+ }
200
+ return lines;
201
+ }
202
+
203
+ /** A generated exercise's answer format, in `lang`; the tutor page gives the rest (tutorpage.ts). */
204
+ function generatedPart(spec: Generated, lang: string, words: Words): string[] {
205
+ const kind = kindOf(spec.answer.compare);
206
+ const unit = kind.kind === "number" && kind.unit !== undefined ? ` (${inLanguage(kind.unit, lang)})` : "";
207
+ return [`- ${words.answerFormat}: ${words.formats[kind.kind === "number" ? "number" : kind.kind]}${unit}`];
208
+ }
209
+
210
+ /**
211
+ * An open task in `lang`: the task, how the learner answers, the points of a good answer as P1, P2 … (each with its
212
+ * kind and, for content, a link to its source from the tutor page, `base` up to the language tree), the model answer,
213
+ * the hint (their Markdown as written, formulas $…$ kept), and how to help.
214
+ */
215
+ function openTaskPart(block: OpenTask, lang: string, words: Words, base: string): string[] {
216
+ const ways = block.answer ?? ["text", "photo"];
217
+ const way = ways.includes("text") && ways.includes("photo") ? "both" : ways.includes("photo") ? "photo" : "text";
218
+ const lines = [
219
+ `- ${words.task}: ${line(inLanguage(block.task, lang))}`,
220
+ `- ${words.answerFormat}: ${words.answerWays[way]}`,
221
+ `- ${words.points}:`,
222
+ ...block.points.map((point, index) => {
223
+ const kind = point.kind === "form" ? words.pointForm : words.pointContent;
224
+ const source =
225
+ point.source === undefined ? "" : ` ${words.source}: [${point.source}](${base}${point.source.slice(1)})`;
226
+ return ` - P${index + 1} (${kind}) ${line(inLanguage(point.text, lang))}${source}`;
227
+ }),
228
+ `- ${words.modelAnswer}: ${line(inLanguage(block.solution, lang))}`,
229
+ ];
230
+ if (block.hint !== undefined) lines.push(`- ${words.hint}: ${line(inLanguage(block.hint, lang))}`);
231
+ lines.push(`- ${words.openGuide}`);
232
+ return lines;
233
+ }
234
+
235
+ /**
236
+ * An exercise component: what it says about itself (its about), its typical mistakes as F1, F2 …, which the app's
237
+ * message of a wrong answer names one of, and when it declares its exercise also its task once as its template; F1, F2 …
238
+ * are then the columns of its table of tasks. Its tasks by number are the tutor page's.
239
+ */
240
+ function componentPart(description: ComponentDescription, lang: string, words: Words): string[] {
241
+ const { about } = description;
242
+ const exercise = description.exercise?.[lang];
243
+ const t = (text: Text) => line(inLanguage(text, lang));
244
+ const lines = [`- ${words.does}: ${t(about.task)}`];
245
+ if (exercise !== undefined) lines.push(`- ${words.task}: „${line(exercise.template)}“`);
246
+ lines.push(`- ${words.answerFormat}: ${t(about.answer)}`, `- ${words.correct}: ${t(about.correct)}`);
247
+ if (about.mistakes.length > 0) {
248
+ lines.push(
249
+ `- ${words.componentMistakes}:`,
250
+ ...about.mistakes.map((mistake, index) => ` - F${index + 1}: ${t(mistake)}`),
251
+ );
252
+ }
253
+ return lines;
254
+ }
255
+
256
+ /** A text of restricted Markdown for a tutor: each link as its words, as its address is the page's (an absolute one stays). */
257
+ const unlinked = (text: string) =>
258
+ text.replace(/\[([^\]]*)\]\(([^)\s]+)\)/g, (whole, words: string, target: string) =>
259
+ /^[a-z][a-z0-9+.-]*:/i.test(target) ? whole : words,
260
+ );
261
+
262
+ /**
263
+ * What a tutor reads of `blocks` in `lang`, as Markdown lines, the content of a page for its tutor page (tutorpage.ts):
264
+ * each section as the heading `heading` makes,
265
+ * then its content: texts, formulas as $…$, definitions, examples and notes with their kind and title, figures by their
266
+ * alt text and caption, tables row by row, timelines, quotes, code, a video as a link with its title and channel, a
267
+ * component's own nojs text; an exercise by its
268
+ * id, as it follows in full after the page. `inline` turns a text into what the page shows. A text keeps its lines,
269
+ * so that its lists stay lists.
270
+ */
271
+ export function contentLines(
272
+ site: Site,
273
+ blocks: readonly Block[],
274
+ lang: string,
275
+ inline: (text: string) => string,
276
+ heading: (title: string, anchor: string, depth: number) => string,
277
+ ): string[] {
278
+ const words = wordsIn(lang);
279
+ const t = (text: Text) => inline(unlinked(inLanguage(text, lang).trim()));
280
+ const one = (text: Text) => t(text).replace(/\s+/g, " ");
281
+ const sentence = (text: Text) => one(text).replace(/[.!?]?$/, (end) => end || ".");
282
+ const lines: string[] = [];
283
+ const add = (...more: string[]) => lines.push(...more, "");
284
+ const walk = (inner: readonly Block[], anchor: string[], depth: number): void => {
285
+ for (const block of inner) {
286
+ switch (block.type) {
287
+ case "section": {
288
+ const id = [...anchor, block.id];
289
+ add(heading(one(block.title), id.join("/"), depth));
290
+ walk(block.blocks, id, depth + 1);
291
+ break;
292
+ }
293
+ case "example":
294
+ case "note":
295
+ add(`**${block.type === "example" ? words.example : words.note}: ${one(block.title)}**`);
296
+ walk(block.blocks, anchor, depth);
297
+ break;
298
+ case "text":
299
+ add(t(block.text));
300
+ break;
301
+ case "formula":
302
+ add(inline(`$${inLanguage(block.formula, lang).replace(/\s+/g, " ").trim()}$`));
303
+ break;
304
+ case "definition":
305
+ add(`- **${one(block.term)}**: ${one(block.explanation)}`);
306
+ break;
307
+ case "figure":
308
+ add(`${words.figure}: ${sentence(block.alt)} ${words.caption}: ${one(block.caption)}`);
309
+ break;
310
+ case "labelled-figure": {
311
+ const labels = block.labels.map((label, index) => `${markerOf(label, index)} ${one(label.text)}`).join(", ");
312
+ const caption = block.caption === undefined ? "" : ` ${words.caption}: ${one(block.caption)}`;
313
+ add(`${words.figure}: ${sentence(block.alt)}${caption} ${words.labels}: ${labels}`);
314
+ break;
315
+ }
316
+ case "table":
317
+ add(
318
+ `${one(block.caption)}:`,
319
+ ...[...(block.header === undefined ? [] : [block.header]), ...block.rows].map(
320
+ (row) => `- ${row.map(one).join(" | ")}`,
321
+ ),
322
+ );
323
+ break;
324
+ case "timeline":
325
+ add(
326
+ ...timelineEvents(site, block).map((event) => {
327
+ const label = event.label === undefined ? "" : ` (${one(event.label)})`;
328
+ const text = event.text === undefined ? "" : ` – ${one(event.text)}`;
329
+ return `- ${String(event.date)}${label}: ${one(event.title)}${text}`;
330
+ }),
331
+ );
332
+ break;
333
+ case "quote": {
334
+ const { origin } = block;
335
+ const from = [origin.author, origin.work, origin.year]
336
+ .filter((part) => part !== undefined)
337
+ .map((part) => (typeof part === "number" ? String(part) : one(part)));
338
+ add(
339
+ ...t(block.text)
340
+ .split("\n")
341
+ .map((text) => `> ${text}`),
342
+ ">",
343
+ `> – ${from.join(", ")}`,
344
+ );
345
+ break;
346
+ }
347
+ case "code":
348
+ add(
349
+ ...(block.caption === undefined ? [] : [`${one(block.caption)}:`, ""]),
350
+ `\`\`\`${block.language}`,
351
+ block.code.trimEnd(),
352
+ "```",
353
+ );
354
+ break;
355
+ case "video":
356
+ add(videoLine(block, words.video));
357
+ break;
358
+ case "vue-component": {
359
+ const use = site.components.find((candidate) => candidate.block === block);
360
+ if (use?.component.exercise === true && use.id !== undefined) {
361
+ add(`- ${words.exerciseHere.replace("{id}", use.id)}`);
362
+ } else if (block.nojs !== undefined) add(`${words.interactive}: ${one(block.nojs)}`);
363
+ break;
364
+ }
365
+ default:
366
+ if ("id" in block && typeof block.id === "string") add(`- ${words.exerciseHere.replace("{id}", block.id)}`);
367
+ }
368
+ }
369
+ };
370
+ walk(blocks, [], 0);
371
+ return lines;
372
+ }
373
+
374
+ /**
375
+ * An exercise for a tutor in `lang`, or undefined for a block that is none: its id, its type, and its lines (task,
376
+ * answer format, solution, hint, typical mistakes; of a generated exercise and an exercise component what stays the
377
+ * same for each number, the tutor page adds the tasks by number).
378
+ */
379
+ export function exercisePart(
380
+ site: Site,
381
+ block: Block,
382
+ lang: string,
383
+ descriptions: Descriptions,
384
+ ): { id: string; type: string; lines: string[] } | undefined {
385
+ const words = wordsIn(lang);
386
+ switch (block.type) {
387
+ case "generated":
388
+ return {
389
+ id: block.id,
390
+ type: `generated, ${kindOf(block.answer.compare).kind}`,
391
+ lines: generatedPart(block, lang, words),
392
+ };
393
+ case "open-task":
394
+ // from the tutor page, <chapter>/<page>/tutor/, three levels up is the language tree
395
+ return { id: block.id, type: "open-task", lines: openTaskPart(block, lang, words, "../../../") };
396
+ case "vue-component": {
397
+ const use = site.components.find((candidate) => candidate.block === block);
398
+ const description = descriptions.get(block);
399
+ if (use?.id === undefined || description === undefined) return undefined;
400
+ return {
401
+ id: use.id,
402
+ type: `${words.component} ${block.component}`,
403
+ lines: componentPart(description, lang, words),
404
+ };
405
+ }
406
+ case "single-choice":
407
+ case "multiple-choice":
408
+ case "true-false":
409
+ case "number":
410
+ case "ordering":
411
+ case "matching":
412
+ return { id: block.id, type: block.type, lines: questionPart(questionIn(block, lang), lang, words) };
413
+ default:
414
+ return undefined;
415
+ }
416
+ }
417
+
418
+ /** The anchors of a page: its sections (nested as <section>/<subsection>), exercises and components with an id. */
419
+ export function anchorsOf(site: Site, page: Page): Set<string> {
420
+ const anchors = new Set<string>();
421
+ const walk = (blocks: readonly Block[], anchor: string[]): void => {
422
+ for (const block of blocks) {
423
+ if (block.type === "section") {
424
+ anchors.add([...anchor, block.id].join("/"));
425
+ walk(block.blocks, [...anchor, block.id]);
426
+ } else if (block.type === "example" || block.type === "note") walk(block.blocks, anchor);
427
+ }
428
+ };
429
+ walk(page.blocks, []);
430
+ for (const block of allBlocks(page.blocks)) {
431
+ if ("id" in block && typeof block.id === "string" && block.type !== "section") anchors.add(block.id);
432
+ }
433
+ for (const use of site.components) {
434
+ if (use.id !== undefined && [...allBlocks(page.blocks)].includes(use.block as BlockOf<"vue-component">)) {
435
+ anchors.add(use.id);
436
+ }
437
+ }
438
+ return anchors;
439
+ }
440
+
441
+ /**
442
+ * The links of a Markdown file at `file` (an address within a language tree, e.g. "prozent/grundwert/tutor/") that
443
+ * lead nowhere: a relative link must name a file the build writes or a page's
444
+ * folder, and its anchor must be on that page. `files` are the files and page folders of the tree, `anchors` each
445
+ * page folder's anchors. One line per broken link.
446
+ */
447
+ export function linkProblems(
448
+ file: string,
449
+ text: string,
450
+ files: ReadonlySet<string>,
451
+ anchors: ReadonlyMap<string, ReadonlySet<string>>,
452
+ ): string[] {
453
+ const problems: string[] = [];
454
+ const base = new URL(file, "https://app.invalid/");
455
+ for (const [, target = ""] of text.matchAll(/\]\(([^)\s]+)\)/g)) {
456
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target)) continue;
457
+ const url = new URL(target, base);
458
+ const path = decodeURIComponent(url.pathname.slice(1));
459
+ if (!files.has(path)) {
460
+ problems.push(`${file}: the link ${target} leads to ${path || "./"}, which the build does not write`);
461
+ continue;
462
+ }
463
+ const id = decodeURIComponent(url.hash.slice(1));
464
+ if (id !== "" && !(anchors.get(path)?.has(id) ?? false)) {
465
+ problems.push(`${file}: the link ${target} names the anchor #${id}, which ${path || "./"} does not have`);
466
+ }
467
+ }
468
+ return problems;
469
+ }