@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,146 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:lernapps:schema:interactive.v1",
4
+ "title": "The start page of a lernapps interactive app",
5
+ "description": "src/app.yaml of an app built from the archetype interactive (runtime @lernapps/app-templates): what the static start page says, readable without JavaScript, the app's languages and its routes, the modes or levels that each have an address app/#<id>. The build validates it against this schema, then what a schema cannot say (languages complete, restricted Markdown, unique ids), and fails with one line per problem. Every text is for learners, in plain language. A text is a string while German is the app's only language, else a map of texts by language: { de: …, en: … }.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["title", "description", "howto", "competencies", "languages"],
9
+ "properties": {
10
+ "title": {
11
+ "description": "The app's title: the h1 of the start page and of the app page, and the title of both.",
12
+ "$ref": "#/$defs/text"
13
+ },
14
+ "description": {
15
+ "description": "What the app does and for whom, under the title of the start page.",
16
+ "$ref": "#/$defs/markdown"
17
+ },
18
+ "howto": {
19
+ "description": "How to play or use the app: the keys, the goal, what counts. The start page shows it under \"So geht's\".",
20
+ "$ref": "#/$defs/markdown"
21
+ },
22
+ "competencies": {
23
+ "description": "What a learner can do after practising with the app, each an \"Ich kann …\" statement. The start page lists them under \"Das lernst du\".",
24
+ "type": "array",
25
+ "minItems": 1,
26
+ "items": {
27
+ "$ref": "#/$defs/text"
28
+ }
29
+ },
30
+ "languages": {
31
+ "description": "The languages of the app, German first. Every text is then complete in each of them; each language other than German gets its own pages below /<language>/.",
32
+ "type": "array",
33
+ "minItems": 1,
34
+ "items": {
35
+ "type": "string",
36
+ "pattern": "^[a-z]{2}$"
37
+ }
38
+ },
39
+ "routes": {
40
+ "description": "Optional: the modes or levels of the app, each with its address app/#<id>, linked from the start page and listed on the app page. The core starts with the route of the address (start(seed, route)), the first one when the address names none. A topic of the app's catalog entry may link one: app/#<id>.",
41
+ "type": "array",
42
+ "minItems": 1,
43
+ "items": {
44
+ "$ref": "#/$defs/route"
45
+ }
46
+ },
47
+ "sections": {
48
+ "description": "Optional: background sections at the end of the start page, each a heading with its text; its id is its anchor.",
49
+ "type": "array",
50
+ "minItems": 1,
51
+ "items": {
52
+ "$ref": "#/$defs/section"
53
+ }
54
+ }
55
+ },
56
+ "$defs": {
57
+ "id": {
58
+ "description": "A stable name, part of the address: lower-case letters, digits and hyphens. Keep it once published, so that links to it keep working.",
59
+ "type": "string",
60
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
61
+ },
62
+ "text": {
63
+ "description": "A text without formatting: a string while German is the only language of the app, else a map of texts by language ({ de: …, en: … }) with every declared language.",
64
+ "anyOf": [
65
+ {
66
+ "type": "string",
67
+ "minLength": 1,
68
+ "pattern": "\\S"
69
+ },
70
+ {
71
+ "type": "object",
72
+ "minProperties": 1,
73
+ "propertyNames": {
74
+ "pattern": "^[a-z]{2}$"
75
+ },
76
+ "additionalProperties": {
77
+ "type": "string",
78
+ "minLength": 1,
79
+ "pattern": "\\S"
80
+ }
81
+ }
82
+ ]
83
+ },
84
+ "markdown": {
85
+ "description": "A text in restricted Markdown (paragraphs, **bold**, *italics*, lists, links, $…$ formulas; no headings, no raw HTML): a string while German is the only language of the app, else a map of texts by language.",
86
+ "anyOf": [
87
+ {
88
+ "type": "string",
89
+ "minLength": 1,
90
+ "pattern": "\\S"
91
+ },
92
+ {
93
+ "type": "object",
94
+ "minProperties": 1,
95
+ "propertyNames": {
96
+ "pattern": "^[a-z]{2}$"
97
+ },
98
+ "additionalProperties": {
99
+ "type": "string",
100
+ "minLength": 1,
101
+ "pattern": "\\S"
102
+ }
103
+ }
104
+ ]
105
+ },
106
+ "route": {
107
+ "description": "A mode or level of the app.",
108
+ "type": "object",
109
+ "additionalProperties": false,
110
+ "required": ["id", "title"],
111
+ "properties": {
112
+ "id": {
113
+ "description": "Its anchor: the address app/#<id>.",
114
+ "$ref": "#/$defs/id"
115
+ },
116
+ "title": {
117
+ "description": "Its name, the text of its links.",
118
+ "$ref": "#/$defs/text"
119
+ },
120
+ "description": {
121
+ "description": "Optional: what it practises, next to its link on the start page.",
122
+ "$ref": "#/$defs/text"
123
+ }
124
+ }
125
+ },
126
+ "section": {
127
+ "description": "A background section of the start page.",
128
+ "type": "object",
129
+ "additionalProperties": false,
130
+ "required": ["id", "title", "text"],
131
+ "properties": {
132
+ "id": {
133
+ "description": "Its anchor on the start page: #<id>.",
134
+ "$ref": "#/$defs/id"
135
+ },
136
+ "title": {
137
+ "description": "Its heading.",
138
+ "$ref": "#/$defs/text"
139
+ },
140
+ "text": {
141
+ "$ref": "#/$defs/markdown"
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,188 @@
1
+ // Reads an interactive app's src/app.yaml and its playthroughs (src/playthroughs/*.yaml) at build time and validates
2
+ // them: each file against its schema (interactive.v1.schema.json, playthrough.v1.schema.json), then what a schema
3
+ // cannot say: German first and each declared language complete, restricted Markdown in the texts, unique ids, at
4
+ // least one playthrough, each step doing something (keys or inputs, not both, or ticks) and expecting something, a
5
+ // playthrough's route one of the app's. The YAML helpers are
6
+ // the explainer's (../explainer/load.ts). Each problem is one line: the file, its line, the place in the file, the
7
+ // language where it matters, and what to do.
8
+ import { existsSync, readdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { Ajv2020, type ValidateFunction } from "ajv/dist/2020.js";
11
+ import schema from "./interactive.v1.schema.json" with { type: "json" };
12
+ import playthroughSchema from "./playthrough.v1.schema.json" with { type: "json" };
13
+ import { Problems, readYaml, schemaProblems, Texts, type YamlFile } from "../shared/content.ts";
14
+ import { LANGUAGES, languageListProblems, type Text } from "../shared/languages.ts";
15
+ import type { Playthrough } from "./contract.ts";
16
+
17
+ /** Where an app finds the schemas of its start page and of its playthroughs, for the messages that name them. */
18
+ const SCHEMA_FILE = "node_modules/@lernapps/app-templates/runtime/interactive/interactive.v1.schema.json";
19
+ const PLAYTHROUGH_SCHEMA_FILE = "node_modules/@lernapps/app-templates/runtime/interactive/playthrough.v1.schema.json";
20
+ export const RULE_URL = "https://lernapps.net/tooling/rules/#interactive-app-valid";
21
+ export const APP = "src/app.yaml";
22
+ export const PLAYTHROUGHS = "src/playthroughs";
23
+
24
+ export interface Route {
25
+ id: string;
26
+ title: Text;
27
+ description?: Text;
28
+ }
29
+
30
+ export interface Section {
31
+ id: string;
32
+ title: Text;
33
+ text: Text;
34
+ }
35
+
36
+ /** src/app.yaml: what the start page says, the languages and the routes. */
37
+ export interface App {
38
+ title: Text;
39
+ description: Text;
40
+ howto: Text;
41
+ competencies: readonly Text[];
42
+ languages: readonly string[];
43
+ routes?: readonly Route[];
44
+ sections?: readonly Section[];
45
+ }
46
+
47
+ /** A playthrough with its file, for the lines of its problems. */
48
+ export interface PlaythroughFile {
49
+ yaml: YamlFile;
50
+ playthrough: Playthrough;
51
+ }
52
+
53
+ export interface Interactive {
54
+ app: App;
55
+ appYaml: YamlFile;
56
+ playthroughs: readonly PlaythroughFile[];
57
+ }
58
+
59
+ /** The app is not valid: one problem per line in `problems`. */
60
+ export class InteractiveError extends Error {
61
+ readonly problems: readonly string[];
62
+ constructor(problems: readonly string[]) {
63
+ super(
64
+ [
65
+ `the interactive app in src/ is not valid (${RULE_URL}):`,
66
+ ...problems.map((problem) => ` - ${problem}`),
67
+ `Fix it as the schemas say: ${SCHEMA_FILE}, ${PLAYTHROUGH_SCHEMA_FILE}`,
68
+ ].join("\n"),
69
+ );
70
+ this.problems = problems;
71
+ }
72
+ }
73
+
74
+ const ajv = new Ajv2020({ allErrors: true, strict: true, allowUnionTypes: true });
75
+ const validators: Record<"app" | "playthrough", ValidateFunction> = {
76
+ app: ajv.compile(schema),
77
+ playthrough: ajv.compile(playthroughSchema),
78
+ };
79
+
80
+ function validate(yaml: YamlFile, kind: keyof typeof validators, problems: Problems): boolean {
81
+ const check = validators[kind];
82
+ if (check(yaml.value)) return true;
83
+ const before = problems.lines.length;
84
+ schemaProblems(yaml, check.errors ?? [], problems);
85
+ if (problems.lines.length === before) problems.at(yaml, [], "does not match the schema");
86
+ return false;
87
+ }
88
+
89
+ /** The problems of a list of ids: each once. */
90
+ function uniqueIds(yaml: YamlFile, key: string, items: readonly { id: string }[], problems: Problems): void {
91
+ items.forEach((item, index) => {
92
+ if (items.findIndex((other) => other.id === item.id) !== index) {
93
+ problems.at(yaml, [key, index, "id"], `${item.id} is used twice in ${key}; give each its own id`);
94
+ }
95
+ });
96
+ }
97
+
98
+ /** The app in `root`, or the problems that keep it from building. */
99
+ export function check(root: string): { interactive?: Interactive; problems: string[] } {
100
+ const problems = new Problems();
101
+ if (!existsSync(join(root, APP))) {
102
+ problems.add(`${APP} is missing: the start page of the app comes from it (${SCHEMA_FILE})`);
103
+ return { problems: problems.lines };
104
+ }
105
+ const appYaml = readYaml(root, APP, problems);
106
+ if (appYaml === undefined || !validate(appYaml, "app", problems)) return { problems: problems.lines };
107
+ const app = appYaml.value as App;
108
+ const languageList = languageListProblems(app.languages, LANGUAGES);
109
+ for (const problem of languageList) {
110
+ problems.at(appYaml, ["languages", Math.max(app.languages.lastIndexOf(problem.lang), 0)], problem.message);
111
+ }
112
+ if (languageList.length > 0) return { problems: problems.lines };
113
+ const texts = new Texts(app.languages, problems);
114
+ texts.check(appYaml, ["title"], "plain");
115
+ texts.check(appYaml, ["description"], "markdown");
116
+ texts.check(appYaml, ["howto"], "markdown");
117
+ app.competencies.forEach((_, index) => texts.check(appYaml, ["competencies", index], "plain"));
118
+ (app.routes ?? []).forEach((_, index) => {
119
+ texts.check(appYaml, ["routes", index, "title"], "plain");
120
+ texts.check(appYaml, ["routes", index, "description"], "plain");
121
+ });
122
+ (app.sections ?? []).forEach((_, index) => {
123
+ texts.check(appYaml, ["sections", index, "title"], "plain");
124
+ texts.check(appYaml, ["sections", index, "text"], "markdown");
125
+ });
126
+ uniqueIds(appYaml, "routes", app.routes ?? [], problems);
127
+ uniqueIds(appYaml, "sections", app.sections ?? [], problems);
128
+ // the playthroughs only of a valid app.yaml: they refer to its routes
129
+ if (problems.lines.length > 0) return { problems: problems.lines };
130
+
131
+ const routes = (app.routes ?? []).map((route) => route.id);
132
+ const files = existsSync(join(root, PLAYTHROUGHS))
133
+ ? readdirSync(join(root, PLAYTHROUGHS))
134
+ .filter((file) => file.endsWith(".yaml"))
135
+ .sort()
136
+ : [];
137
+ if (files.length === 0) {
138
+ problems.add(
139
+ `${PLAYTHROUGHS}/: no playthrough; write at least one, <name>.yaml: a seed, and steps of keys, inputs or ticks with the state or announcement expected after them (${PLAYTHROUGH_SCHEMA_FILE})`,
140
+ );
141
+ }
142
+ const playthroughs: PlaythroughFile[] = [];
143
+ for (const file of files) {
144
+ const yaml = readYaml(root, `${PLAYTHROUGHS}/${file}`, problems);
145
+ if (yaml === undefined || !validate(yaml, "playthrough", problems)) continue;
146
+ const playthrough = yaml.value as Playthrough;
147
+ if (playthrough.route !== undefined && !routes.includes(playthrough.route)) {
148
+ problems.at(
149
+ yaml,
150
+ ["route"],
151
+ routes.length === 0
152
+ ? `${playthrough.route}: ${APP} has no routes; leave route out`
153
+ : `${playthrough.route} is not a route of ${APP}; use one of ${routes.join(", ")}`,
154
+ );
155
+ }
156
+ playthrough.steps.forEach((step, index) => {
157
+ if (step.keys !== undefined && step.inputs !== undefined) {
158
+ problems.at(
159
+ yaml,
160
+ ["steps", index, "inputs"],
161
+ "keys and inputs in one step; write the keys as key inputs ({ kind: key, key: a }) in inputs, or give each its own step",
162
+ );
163
+ } else if (step.keys === undefined && step.inputs === undefined && step.ticks === undefined) {
164
+ problems.at(
165
+ yaml,
166
+ ["steps", index],
167
+ "does nothing; give the keys (keys) or inputs (inputs) of the step, the ticks that pass (ticks), or both",
168
+ );
169
+ } else if (step.state === undefined && step.announce === undefined) {
170
+ problems.at(
171
+ yaml,
172
+ ["steps", index],
173
+ "expects nothing; give the state expected after the step (state), the announcement (announce), or both",
174
+ );
175
+ }
176
+ });
177
+ playthroughs.push({ yaml, playthrough });
178
+ }
179
+ if (problems.lines.length > 0) return { problems: problems.lines };
180
+ return { interactive: { app, appYaml, playthroughs }, problems: [] };
181
+ }
182
+
183
+ /** The app in `root`; an InteractiveError with every problem when it is not valid. */
184
+ export function loadInteractive(root: string): Interactive {
185
+ const { interactive, problems } = check(root);
186
+ if (interactive === undefined) throw new InteractiveError(problems);
187
+ return interactive;
188
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "language": "Deutsch",
3
+ "languages": "Sprache",
4
+ "play": "Los geht's",
5
+ "routes": "Auswahl",
6
+ "howto": "So geht's",
7
+ "competencies": "Das lernst du",
8
+ "noscript": "Für die App brauchst du JavaScript. Schalte es in deinem Browser ein. Alles andere auf dieser Seite kannst du auch ohne lesen.",
9
+ "noscriptApp": "Diese App braucht JavaScript. Schalte es in deinem Browser ein. Was die App macht, steht auf der Startseite.",
10
+ "toStart": "Zur Startseite",
11
+ "footer": "Eine interaktive App auf lernapps.net",
12
+ "imprint": "Impressum",
13
+ "privacy": "Datenschutz"
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "language": "English",
3
+ "languages": "Language",
4
+ "play": "Let's go",
5
+ "routes": "Choose",
6
+ "howto": "How it works",
7
+ "competencies": "What you learn",
8
+ "noscript": "The app needs JavaScript. Turn it on in your browser. You can read everything else on this page without it.",
9
+ "noscriptApp": "This app needs JavaScript. Turn it on in your browser. The start page says what the app does.",
10
+ "toStart": "To the start page",
11
+ "footer": "An interactive app on lernapps.net",
12
+ "imprint": "Imprint",
13
+ "privacy": "Privacy"
14
+ }
@@ -0,0 +1,11 @@
1
+ // The interactive shell's own texts for learners, in each language it has them: an app may declare only these
2
+ // languages.
3
+ import de from "./messages/de.json" with { type: "json" };
4
+ import en from "./messages/en.json" with { type: "json" };
5
+ import { textsIn, type Language } from "../shared/languages.ts";
6
+
7
+ export type Messages = typeof de;
8
+ export const MESSAGES: Readonly<Record<Language, Messages>> = { de, en };
9
+
10
+ /** The text of `key` in `lang`. */
11
+ export const message = (lang: string, key: keyof Messages): string => textsIn(MESSAGES, lang)[key];
@@ -0,0 +1,143 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:lernapps:schema:playthrough.v1",
4
+ "title": "A playthrough of a lernapps interactive app",
5
+ "description": "A file src/playthroughs/<name>.yaml of an app built from the archetype interactive: what a learner does, from the start of a route with a seed: inputs (keys typed, actions of the app's root component) and time passing (ticks), and after each step what is expected: the state of the core (the fields named, compared with the state step(state, input) and tick(state, dt) give) or the announcement for screen readers (@lernapps/tooling/a11y), or both. The build plays every playthrough against the core and fails with one line per mismatch; the end-to-end tests play the same inputs and ticks in the browser. In both, no time passes but the ticks a step names.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["seed", "steps"],
9
+ "properties": {
10
+ "route": {
11
+ "description": "The route it starts at, an id of routes in src/app.yaml; the first route when it names none.",
12
+ "type": "string",
13
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
14
+ },
15
+ "seed": {
16
+ "description": "The seed it starts with: start(seed, route); in the browser the address app/?seed=<seed>#<route>.",
17
+ "type": "integer",
18
+ "minimum": 1,
19
+ "maximum": 2147483647
20
+ },
21
+ "steps": {
22
+ "description": "The steps, in order.",
23
+ "type": "array",
24
+ "minItems": 1,
25
+ "items": {
26
+ "$ref": "#/$defs/step"
27
+ }
28
+ }
29
+ },
30
+ "$defs": {
31
+ "step": {
32
+ "description": "What happens: keys or inputs, then ticks, at least one of them; then what is expected after them: state, announce, or both.",
33
+ "type": "object",
34
+ "additionalProperties": false,
35
+ "properties": {
36
+ "keys": {
37
+ "description": "Keys, one after the other, each as the browser names it (KeyboardEvent.key): a, A, Enter, \" \" for the space bar, ArrowLeft. The short form of inputs that are all keys; a step has keys or inputs, not both.",
38
+ "type": "array",
39
+ "minItems": 1,
40
+ "items": {
41
+ "type": "string",
42
+ "minLength": 1
43
+ }
44
+ },
45
+ "inputs": {
46
+ "description": "Inputs, one after the other: a key ({ kind: key, key: a }), pressed on the keyboard in the browser, or an action of the app's root component ({ kind: action, name: again }, with a value if it has one), emitted by the root component as input in the browser.",
47
+ "type": "array",
48
+ "minItems": 1,
49
+ "items": {
50
+ "$ref": "#/$defs/input"
51
+ }
52
+ },
53
+ "ticks": {
54
+ "description": "The ticks that pass after the keys or inputs: tick(state, TICK) that many times, TICK = 20 ms, so 50 ticks are a second.",
55
+ "type": "integer",
56
+ "minimum": 1
57
+ },
58
+ "state": {
59
+ "description": "The state expected after the step: the fields named, each compared in the same way; a list or any other value is compared as a whole."
60
+ },
61
+ "announce": {
62
+ "description": "The announcement expected after the step, in German: what @lernapps/tooling/a11y's announce said last. Only the browser can see it.",
63
+ "type": "string",
64
+ "minLength": 1,
65
+ "pattern": "\\S"
66
+ }
67
+ }
68
+ },
69
+ "input": {
70
+ "description": "An input to the core: step(state, input). Told apart by kind.",
71
+ "type": "object",
72
+ "required": ["kind"],
73
+ "properties": {
74
+ "kind": {
75
+ "enum": ["key", "action"]
76
+ }
77
+ },
78
+ "allOf": [
79
+ {
80
+ "if": {
81
+ "type": "object",
82
+ "properties": {
83
+ "kind": {
84
+ "const": "key"
85
+ }
86
+ }
87
+ },
88
+ "then": {
89
+ "$ref": "#/$defs/key"
90
+ }
91
+ },
92
+ {
93
+ "if": {
94
+ "type": "object",
95
+ "properties": {
96
+ "kind": {
97
+ "const": "action"
98
+ }
99
+ }
100
+ },
101
+ "then": {
102
+ "$ref": "#/$defs/action"
103
+ }
104
+ }
105
+ ]
106
+ },
107
+ "key": {
108
+ "description": "A key, as the browser names it (KeyboardEvent.key).",
109
+ "type": "object",
110
+ "additionalProperties": false,
111
+ "required": ["kind", "key"],
112
+ "properties": {
113
+ "kind": {
114
+ "const": "key"
115
+ },
116
+ "key": {
117
+ "type": "string",
118
+ "minLength": 1
119
+ }
120
+ }
121
+ },
122
+ "action": {
123
+ "description": "An action the app's root component emits as input, for what the learner does with the pointer or by touch.",
124
+ "type": "object",
125
+ "additionalProperties": false,
126
+ "required": ["kind", "name"],
127
+ "properties": {
128
+ "kind": {
129
+ "const": "action"
130
+ },
131
+ "name": {
132
+ "description": "The name the app gives the action.",
133
+ "type": "string",
134
+ "minLength": 1,
135
+ "pattern": "\\S"
136
+ },
137
+ "value": {
138
+ "description": "The JSON value the action carries, if it needs one."
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }