@lernapps/app-templates 0.0.1 → 0.1.1
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.
- package/LICENSE +22 -0
- package/README.md +195 -2
- package/dist/appname-Bh23cUuz.mjs +12 -0
- package/dist/assessment-Dy359O1O.mjs +1696 -0
- package/dist/block-BK_ohsH1.mjs +82 -0
- package/dist/blocks-CxtCPAa5.mjs +149 -0
- package/dist/blocks.mjs +4 -0
- package/dist/check-CteIFN96.mjs +254 -0
- package/dist/content-DO6oEJxj.mjs +140 -0
- package/dist/contract-PONpxJtd.mjs +52 -0
- package/dist/e2e-CU0n_unV.mjs +89 -0
- package/dist/engine-CArxqizW.mjs +136 -0
- package/dist/explainer-e2e.mjs +1311 -0
- package/dist/explainer-islands.mjs +197 -0
- package/dist/explainer-plugin.mjs +2132 -0
- package/dist/explainer.mjs +1246 -0
- package/dist/html-mffkHGHh.mjs +89 -0
- package/dist/image-CspJVfyQ.mjs +21 -0
- package/dist/interactive-e2e.mjs +140 -0
- package/dist/interactive-plugin.mjs +242 -0
- package/dist/interactive.mjs +75 -0
- package/dist/load-RosvaPZf.mjs +16976 -0
- package/dist/needs-BzEahJ6X.mjs +97 -0
- package/dist/pages-9RaaomoO.mjs +81 -0
- package/dist/questions-e2e.mjs +1 -0
- package/dist/questions-engine.mjs +3 -0
- package/dist/questions.mjs +6 -0
- package/dist/quiz-DnW-xtCh.mjs +70 -0
- package/dist/quiz-e2e.mjs +174 -0
- package/dist/quiz-plugin.mjs +266 -0
- package/dist/quiz.mjs +246 -0
- package/dist/render-D5kwkNt1.mjs +507 -0
- package/dist/render-DqHOJyHK.mjs +146 -0
- package/dist/render-GS1Htlu8.mjs +545 -0
- package/dist/result-D_mmCVwN.mjs +180 -0
- package/dist/tasks-DADUYy52.mjs +74 -0
- package/dist/validate-BDwL7Su3.mjs +1076 -0
- package/dist/validate-Bxy9NfB8.mjs +146 -0
- package/dist/vue-DO1m6q1R.mjs +40 -0
- package/dist/vue-code-BkJjW_I_.mjs +320 -0
- package/docs/arc42/01-introduction-and-goals.arc42.md +82 -0
- package/docs/arc42/02-constraints.arc42.md +228 -0
- package/docs/arc42/03-system-context.arc42.md +118 -0
- package/docs/arc42/04-solution-strategy.arc42.md +47 -0
- package/docs/arc42/05-building-blocks.arc42.md +307 -0
- package/docs/arc42/06-runtime-view.arc42.md +98 -0
- package/docs/arc42/07-deployment-view.arc42.md +160 -0
- package/docs/arc42/08-concepts.arc42.md +123 -0
- package/docs/arc42/09-decisions.arc42.md +246 -0
- package/docs/arc42/10-quality-requirements.arc42.md +155 -0
- package/docs/arc42/11-risks.arc42.md +113 -0
- package/docs/arc42/12-glossary.arc42.md +162 -0
- package/docs/arc42/architecture-evidence.md +395 -0
- package/explainer/.arc42ignore +3 -0
- package/explainer/README.md +54 -0
- package/explainer/_gitignore +5 -0
- package/explainer/docs/arc42/03-explainer-system-context.arc42.md +35 -0
- package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +659 -0
- package/explainer/docs/arc42/06-explainer-runtime-view.arc42.md +105 -0
- package/explainer/docs/arc42/07-explainer-deployment-view.arc42.md +34 -0
- package/explainer/docs/arc42/08-explainer-concepts.arc42.md +17 -0
- package/explainer/docs/arc42/09-explainer-decisions.arc42.md +443 -0
- package/explainer/docs/arc42/10-explainer-quality-requirements.arc42.md +54 -0
- package/explainer/docs/arc42/11-explainer-risks.arc42.md +17 -0
- package/explainer/docs/arc42/12-explainer-glossary.arc42.md +185 -0
- package/explainer/e2e/explainer.e2e.ts +3 -0
- package/explainer/index.html +11 -0
- package/explainer/package.json +18 -0
- package/explainer/playwright.config.ts +1 -0
- package/explainer/src/app.yaml +20 -0
- package/explainer/src/components/ProzentFeld.test.ts +30 -0
- package/explainer/src/components/ProzentFeld.ts +121 -0
- package/explainer/src/components/ProzentFeld.vue +84 -0
- package/explainer/src/images/hunderterfeld-klammern.svg +105 -0
- package/explainer/src/main.ts +4 -0
- package/explainer/src/prozent/chapter.yaml +18 -0
- package/explainer/src/prozent/grundwert.yaml +115 -0
- package/explainer/src/prozent/prozentsatz.yaml +99 -0
- package/explainer/src/prozent/prozentwert.yaml +119 -0
- package/explainer/tsconfig.json +1 -0
- package/explainer/vite.config.ts +4 -0
- package/interactive/.arc42ignore +5 -0
- package/interactive/README.md +28 -0
- package/interactive/_gitignore +4 -0
- package/interactive/docs/arc42/05-interactive-building-blocks.arc42.md +518 -0
- package/interactive/docs/arc42/06-interactive-runtime-view.arc42.md +58 -0
- package/interactive/docs/arc42/07-interactive-deployment-view.arc42.md +35 -0
- package/interactive/docs/arc42/08-interactive-concepts.arc42.md +52 -0
- package/interactive/docs/arc42/09-interactive-decisions.arc42.md +130 -0
- package/interactive/docs/arc42/10-interactive-quality-requirements.arc42.md +45 -0
- package/interactive/docs/arc42/11-interactive-risks.arc42.md +20 -0
- package/interactive/docs/arc42/12-interactive-glossary.arc42.md +80 -0
- package/interactive/e2e/interactive.e2e.ts +3 -0
- package/interactive/index.html +11 -0
- package/interactive/package.json +18 -0
- package/interactive/playwright.config.ts +1 -0
- package/interactive/src/app.yaml +28 -0
- package/interactive/src/core/index.ts +101 -0
- package/interactive/src/data/lessons.json +4 -0
- package/interactive/src/env.d.ts +6 -0
- package/interactive/src/main.ts +6 -0
- package/interactive/src/messages/de.json +12 -0
- package/interactive/src/playthroughs/grundreihe.yaml +33 -0
- package/interactive/src/playthroughs/obere-reihe.yaml +14 -0
- package/interactive/src/ui/App.vue +85 -0
- package/interactive/tsconfig.json +1 -0
- package/interactive/vite.config.ts +4 -0
- package/package.json +143 -4
- package/quiz/README.md +31 -0
- package/quiz/_gitignore +5 -0
- package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +283 -0
- package/quiz/docs/arc42/06-quiz-runtime-view.arc42.md +21 -0
- package/quiz/docs/arc42/07-quiz-deployment-view.arc42.md +33 -0
- package/quiz/docs/arc42/09-quiz-decisions.arc42.md +36 -0
- package/quiz/docs/arc42/10-quiz-quality-requirements.arc42.md +53 -0
- package/quiz/docs/arc42/11-quiz-risks.arc42.md +19 -0
- package/quiz/docs/arc42/12-quiz-glossary.arc42.md +27 -0
- package/quiz/e2e/quiz.e2e.ts +3 -0
- package/quiz/index.html +11 -0
- package/quiz/package.json +18 -0
- package/quiz/playwright.config.ts +1 -0
- package/quiz/src/images/berge-ordnen.jpg +0 -0
- package/quiz/src/images/everest-hoehe.jpg +0 -0
- package/quiz/src/images/galapagos.jpg +0 -0
- package/quiz/src/images/grand-canyon.jpg +0 -0
- package/quiz/src/images/totes-meer.jpg +0 -0
- package/quiz/src/images/uluru.jpg +0 -0
- package/quiz/src/images/wasserfaelle-laender.jpg +0 -0
- package/quiz/src/main.ts +4 -0
- package/quiz/src/quiz.json +312 -0
- package/quiz/tsconfig.json +1 -0
- package/quiz/vite.config.ts +4 -0
- package/runtime/explainer/assessment-client.ts +478 -0
- package/runtime/explainer/assessment-page.ts +448 -0
- package/runtime/explainer/assessment.ts +393 -0
- package/runtime/explainer/check-page.ts +520 -0
- package/runtime/explainer/components/exercise.ts +409 -0
- package/runtime/explainer/components/props.ts +230 -0
- package/runtime/explainer/components/read.ts +428 -0
- package/runtime/explainer/content.ts +277 -0
- package/runtime/explainer/e2e.ts +1488 -0
- package/runtime/explainer/exercises.ts +326 -0
- package/runtime/explainer/explainer.css +1283 -0
- package/runtime/explainer/explainer.ts +838 -0
- package/runtime/explainer/explainer.v1.schema.json +1165 -0
- package/runtime/explainer/expression.ts +407 -0
- package/runtime/explainer/fraction.ts +107 -0
- package/runtime/explainer/generated/comparator.ts +31 -0
- package/runtime/explainer/generated/question.ts +165 -0
- package/runtime/explainer/generated/soundness.ts +280 -0
- package/runtime/explainer/generated/texts.ts +257 -0
- package/runtime/explainer/generated/types.ts +103 -0
- package/runtime/explainer/generated/values.ts +136 -0
- package/runtime/explainer/islands.ts +281 -0
- package/runtime/explainer/load.ts +457 -0
- package/runtime/explainer/messages/de.json +200 -0
- package/runtime/explainer/messages/en.json +200 -0
- package/runtime/explainer/messages.ts +13 -0
- package/runtime/explainer/needs.ts +122 -0
- package/runtime/explainer/open-task.ts +235 -0
- package/runtime/explainer/outline.ts +306 -0
- package/runtime/explainer/plugin.ts +488 -0
- package/runtime/explainer/position.ts +62 -0
- package/runtime/explainer/prompt.ts +75 -0
- package/runtime/explainer/render.ts +686 -0
- package/runtime/explainer/result.ts +238 -0
- package/runtime/explainer/routes.ts +119 -0
- package/runtime/explainer/schema.ts +140 -0
- package/runtime/explainer/scope.ts +87 -0
- package/runtime/explainer/stuck.ts +57 -0
- package/runtime/explainer/tasks.ts +109 -0
- package/runtime/explainer/tutor.ts +469 -0
- package/runtime/explainer/tutorpage.ts +669 -0
- package/runtime/explainer/video.ts +123 -0
- package/runtime/interactive/contract.ts +121 -0
- package/runtime/interactive/e2e.ts +190 -0
- package/runtime/interactive/interactive.css +82 -0
- package/runtime/interactive/interactive.ts +115 -0
- package/runtime/interactive/interactive.v1.schema.json +146 -0
- package/runtime/interactive/load.ts +188 -0
- package/runtime/interactive/messages/de.json +14 -0
- package/runtime/interactive/messages/en.json +14 -0
- package/runtime/interactive/messages.ts +11 -0
- package/runtime/interactive/playthrough.v1.schema.json +143 -0
- package/runtime/interactive/plugin.ts +266 -0
- package/runtime/interactive/render.ts +126 -0
- package/runtime/quiz/e2e.ts +233 -0
- package/runtime/quiz/engine.ts +242 -0
- package/runtime/quiz/messages/de.json +23 -0
- package/runtime/quiz/messages/en.json +23 -0
- package/runtime/quiz/plugin.ts +150 -0
- package/runtime/quiz/quiz.css +154 -0
- package/runtime/quiz/quiz.ts +56 -0
- package/runtime/quiz/quiz.v1.schema.json +72 -0
- package/runtime/quiz/render.ts +173 -0
- package/runtime/quiz/validate.ts +108 -0
- package/runtime/shared/appname.ts +16 -0
- package/runtime/shared/blocks/block.ts +162 -0
- package/runtime/shared/blocks/blocks.css +190 -0
- package/runtime/shared/blocks/blocks.v1.schema.json +432 -0
- package/runtime/shared/blocks/code.ts +26 -0
- package/runtime/shared/blocks/figures.ts +49 -0
- package/runtime/shared/blocks/image.ts +25 -0
- package/runtime/shared/blocks/index.ts +13 -0
- package/runtime/shared/blocks/messages/de.json +4 -0
- package/runtime/shared/blocks/messages/en.json +4 -0
- package/runtime/shared/blocks/render.ts +134 -0
- package/runtime/shared/blocks/validate.ts +165 -0
- package/runtime/shared/content.ts +197 -0
- package/runtime/shared/examples.ts +20 -0
- package/runtime/shared/html.ts +10 -0
- package/runtime/shared/languages.ts +94 -0
- package/runtime/shared/page.css +101 -0
- package/runtime/shared/pages.ts +114 -0
- package/runtime/shared/questions/e2e.ts +115 -0
- package/runtime/shared/questions/engine.ts +209 -0
- package/runtime/shared/questions/index.ts +39 -0
- package/runtime/shared/questions/messages/de.json +26 -0
- package/runtime/shared/questions/messages/en.json +26 -0
- package/runtime/shared/questions/question.ts +433 -0
- package/runtime/shared/questions/question.v1.schema.json +346 -0
- package/runtime/shared/questions/questions.css +192 -0
- package/runtime/shared/questions/render.ts +255 -0
- package/runtime/shared/questions/validate.ts +518 -0
- package/runtime/shared/schema.ts +44 -0
- package/runtime/shared/text/check.ts +68 -0
- package/runtime/shared/text/formula.ts +69 -0
- package/runtime/shared/text/markdown.ts +185 -0
- package/runtime/shared/text/speech.ts +39 -0
- package/runtime/shared/text/text.v1.schema.json +34 -0
- package/runtime/shared/vue-code.ts +395 -0
- package/runtime/shared/vue.ts +38 -0
|
@@ -0,0 +1,1076 @@
|
|
|
1
|
+
import { c as textLanguages } from "./html-mffkHGHh.mjs";
|
|
2
|
+
import { n as checkMaterial, t as MATERIAL_DEFINITIONS } from "./validate-Bxy9NfB8.mjs";
|
|
3
|
+
import { _ as questionIn, h as questionAddresses, l as isComplete, m as parseNumber, u as isCorrect, v as questionTexts, x as rightEntries } from "./render-GS1Htlu8.mjs";
|
|
4
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
5
|
+
// runtime/shared/examples.ts
|
|
6
|
+
/**
|
|
7
|
+
* What to do about an example the check judges otherwise than it is listed: `right`, whether the check accepts it;
|
|
8
|
+
* `check`, what judges it; `fix`, what to change so that the check agrees.
|
|
9
|
+
*/
|
|
10
|
+
function verdict(example, right, check = "the check", fix = "change the exercise so that its check") {
|
|
11
|
+
return right ? `${check} accepts ${example}, but it is listed as rejected; if the task's text rules it out, ${fix} rejects it, else move it to accepted` : `${check} rejects ${example}, but it is listed as accepted; if the task's text allows it, ${fix} accepts it, else move it to rejected`;
|
|
12
|
+
}
|
|
13
|
+
// runtime/shared/schema.ts
|
|
14
|
+
/** The schemas the given ones are made of, by their $id. */
|
|
15
|
+
const schemasById = (...schemas) => Object.fromEntries(schemas.map((schema) => [schema.$id, schema]));
|
|
16
|
+
/**
|
|
17
|
+
* The properties of the definition `name` of the schema `id`, and of everything it refers to; with `branches` false
|
|
18
|
+
* not those of the branches of its allOf (the fields that only one type of a question has).
|
|
19
|
+
*/
|
|
20
|
+
function knownProperties(schemas, id, name, { branches = true } = {}) {
|
|
21
|
+
const into = /* @__PURE__ */ new Set();
|
|
22
|
+
const visit = (definition, scope) => {
|
|
23
|
+
if (typeof definition !== "object" || definition === null) return;
|
|
24
|
+
const def = definition;
|
|
25
|
+
for (const key of Object.keys(def.properties ?? {})) into.add(key);
|
|
26
|
+
if (typeof def.$ref === "string") {
|
|
27
|
+
const [target = "", fragment = ""] = def.$ref.split("#");
|
|
28
|
+
const there = target === "" ? scope : target;
|
|
29
|
+
const found = (schemas[there]?.$defs)?.[fragment.replace("/$defs/", "")];
|
|
30
|
+
visit(found, there);
|
|
31
|
+
}
|
|
32
|
+
for (const part of def.allOf ?? []) {
|
|
33
|
+
const branch = typeof part === "object" && part !== null && "then" in part;
|
|
34
|
+
if (branch && !branches) continue;
|
|
35
|
+
visit(part, scope);
|
|
36
|
+
if (branch) visit(part.then, scope);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
visit((schemas[id]?.$defs)?.[name], id);
|
|
40
|
+
return into;
|
|
41
|
+
}
|
|
42
|
+
// runtime/shared/blocks/blocks.v1.schema.json
|
|
43
|
+
var blocks_v1_schema_default = {
|
|
44
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
45
|
+
$id: "urn:lernapps:schema:blocks.v1",
|
|
46
|
+
title: "Blocks that show content",
|
|
47
|
+
description: "The blocks the quiz shows with a question and the explainer shows on a page alike: prose, a formula, a figure (drawn from data or a bundled image), a labelled figure (a bundled image with numbered labels), a table, a quotation and program code, with the credit of an image or a quotation. An image is a file in src/images/ of the app.",
|
|
48
|
+
$defs: {
|
|
49
|
+
"credit": {
|
|
50
|
+
"description": "Where a bundled image or a quote comes from: the source (a URL or a citation) and its licence (an SPDX identifier such as CC-BY-SA-4.0, or \"gemeinfrei\").",
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {
|
|
53
|
+
"source": {
|
|
54
|
+
"description": "Where it comes from: a URL, the same in every language and shown as a link, or a citation, a text like any other: a string while German is the only language of the app, else a map of texts by language with every declared language, e.g. { de: eigene Zeichnung, en: own drawing }. Each language's page shows its own.",
|
|
55
|
+
"anyOf": [{
|
|
56
|
+
"type": "string",
|
|
57
|
+
"minLength": 1,
|
|
58
|
+
"pattern": "\\S"
|
|
59
|
+
}, {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"minProperties": 1,
|
|
62
|
+
"propertyNames": { "pattern": "^[a-z]{2}$" },
|
|
63
|
+
"additionalProperties": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"minLength": 1,
|
|
66
|
+
"pattern": "\\S"
|
|
67
|
+
}
|
|
68
|
+
}]
|
|
69
|
+
},
|
|
70
|
+
"licence": {
|
|
71
|
+
"description": "Its licence: an SPDX identifier (CC-BY-SA-4.0, CC0-1.0, …), or gemeinfrei for a work in the public domain.",
|
|
72
|
+
"type": "string",
|
|
73
|
+
"minLength": 1,
|
|
74
|
+
"pattern": "\\S"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"image": {
|
|
79
|
+
"description": "A bundled image file in src/images/, as a path from src/: images/<name>.svg (or .png, .jpg, .webp).",
|
|
80
|
+
"type": "string",
|
|
81
|
+
"pattern": "^images/[a-z0-9]+(-[a-z0-9]+)*\\.(svg|png|jpg|webp)$"
|
|
82
|
+
},
|
|
83
|
+
"drawing": {
|
|
84
|
+
"description": "A figure drawn from numbers at build, as inline SVG: hundred-grid (a field of 100 squares, filled of them shaded) or bar (a strip, part of whole shaded, with its percentage).",
|
|
85
|
+
"type": "object",
|
|
86
|
+
"required": ["kind"],
|
|
87
|
+
"properties": { "kind": { "enum": ["hundred-grid", "bar"] } },
|
|
88
|
+
"allOf": [{
|
|
89
|
+
"if": {
|
|
90
|
+
"properties": { "kind": { "const": "hundred-grid" } },
|
|
91
|
+
"required": ["kind"],
|
|
92
|
+
"type": "object"
|
|
93
|
+
},
|
|
94
|
+
"then": {
|
|
95
|
+
"required": ["filled"],
|
|
96
|
+
"properties": { "filled": {
|
|
97
|
+
"description": "How many of the 100 squares are shaded.",
|
|
98
|
+
"type": "integer",
|
|
99
|
+
"minimum": 0,
|
|
100
|
+
"maximum": 100
|
|
101
|
+
} },
|
|
102
|
+
"type": "object"
|
|
103
|
+
}
|
|
104
|
+
}, {
|
|
105
|
+
"if": {
|
|
106
|
+
"properties": { "kind": { "const": "bar" } },
|
|
107
|
+
"required": ["kind"],
|
|
108
|
+
"type": "object"
|
|
109
|
+
},
|
|
110
|
+
"then": {
|
|
111
|
+
"required": ["part", "whole"],
|
|
112
|
+
"properties": {
|
|
113
|
+
"part": {
|
|
114
|
+
"description": "The shaded part.",
|
|
115
|
+
"type": "number",
|
|
116
|
+
"minimum": 0
|
|
117
|
+
},
|
|
118
|
+
"whole": {
|
|
119
|
+
"description": "The whole strip, 100 %.",
|
|
120
|
+
"type": "number",
|
|
121
|
+
"exclusiveMinimum": 0
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"type": "object"
|
|
125
|
+
}
|
|
126
|
+
}],
|
|
127
|
+
"unevaluatedProperties": false
|
|
128
|
+
},
|
|
129
|
+
"figure": {
|
|
130
|
+
"description": "A figure: drawn from data (draw) or a bundled image (image, with its source and licence), one of the two. It has an alternative text that says what it shows, and a caption that says what to see.",
|
|
131
|
+
"required": ["alt", "caption"],
|
|
132
|
+
"allOf": [{ "$ref": "#/$defs/credit" }],
|
|
133
|
+
"properties": {
|
|
134
|
+
"draw": { "$ref": "#/$defs/drawing" },
|
|
135
|
+
"image": { "$ref": "#/$defs/image" },
|
|
136
|
+
"alt": {
|
|
137
|
+
"description": "What the figure shows, for those who cannot see it.",
|
|
138
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
139
|
+
},
|
|
140
|
+
"caption": {
|
|
141
|
+
"description": "What to see in the figure, under it.",
|
|
142
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/markdown"
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
"dependentRequired": { "image": ["source", "licence"] },
|
|
146
|
+
"type": "object"
|
|
147
|
+
},
|
|
148
|
+
"labelledFigure": {
|
|
149
|
+
"description": "A bundled image with labels at positions. Each label shows its marker (A, B, 1a; else its number in order). The texts of the labels are listed beside the image (the legend), unless legend is false.",
|
|
150
|
+
"required": [
|
|
151
|
+
"image",
|
|
152
|
+
"alt",
|
|
153
|
+
"labels",
|
|
154
|
+
"source",
|
|
155
|
+
"licence"
|
|
156
|
+
],
|
|
157
|
+
"allOf": [{ "$ref": "#/$defs/credit" }],
|
|
158
|
+
"properties": {
|
|
159
|
+
"image": { "$ref": "#/$defs/image" },
|
|
160
|
+
"alt": {
|
|
161
|
+
"description": "What the image shows, for those who cannot see it.",
|
|
162
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
163
|
+
},
|
|
164
|
+
"caption": {
|
|
165
|
+
"description": "What to see in the figure.",
|
|
166
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/markdown"
|
|
167
|
+
},
|
|
168
|
+
"legend": {
|
|
169
|
+
"description": "Whether the texts of the labels are listed beside the image. Default true. With false the figure shows only the markers (for a figure the learner labels), and alt names every marker with its part.",
|
|
170
|
+
"type": "boolean"
|
|
171
|
+
},
|
|
172
|
+
"labels": {
|
|
173
|
+
"type": "array",
|
|
174
|
+
"minItems": 1,
|
|
175
|
+
"items": {
|
|
176
|
+
"type": "object",
|
|
177
|
+
"additionalProperties": false,
|
|
178
|
+
"required": [
|
|
179
|
+
"x",
|
|
180
|
+
"y",
|
|
181
|
+
"text"
|
|
182
|
+
],
|
|
183
|
+
"properties": {
|
|
184
|
+
"marker": {
|
|
185
|
+
"description": "What the label shows on the image, plain text of 1 to 3 characters (A, B, 1a), the letters of the material. Each marker is used once. Without it: the number of the label in order, from 1.",
|
|
186
|
+
"type": "string",
|
|
187
|
+
"minLength": 1,
|
|
188
|
+
"maxLength": 3
|
|
189
|
+
},
|
|
190
|
+
"x": {
|
|
191
|
+
"description": "From the left edge, in percent of the width.",
|
|
192
|
+
"type": "number",
|
|
193
|
+
"minimum": 0,
|
|
194
|
+
"maximum": 100
|
|
195
|
+
},
|
|
196
|
+
"y": {
|
|
197
|
+
"description": "From the top edge, in percent of the height.",
|
|
198
|
+
"type": "number",
|
|
199
|
+
"minimum": 0,
|
|
200
|
+
"maximum": 100
|
|
201
|
+
},
|
|
202
|
+
"text": {
|
|
203
|
+
"description": "What the label names.",
|
|
204
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
"type": "object"
|
|
211
|
+
},
|
|
212
|
+
"materialBlock": {
|
|
213
|
+
"description": "A block that shows content with a question: a figure or a labelled figure.",
|
|
214
|
+
"type": "object",
|
|
215
|
+
"required": ["type"],
|
|
216
|
+
"properties": { "type": {
|
|
217
|
+
"description": "The kind of block.",
|
|
218
|
+
"enum": [
|
|
219
|
+
"text",
|
|
220
|
+
"formula",
|
|
221
|
+
"figure",
|
|
222
|
+
"labelled-figure",
|
|
223
|
+
"table",
|
|
224
|
+
"quote",
|
|
225
|
+
"code"
|
|
226
|
+
]
|
|
227
|
+
} },
|
|
228
|
+
"allOf": [
|
|
229
|
+
{
|
|
230
|
+
"if": {
|
|
231
|
+
"properties": { "type": { "const": "text" } },
|
|
232
|
+
"required": ["type"],
|
|
233
|
+
"type": "object"
|
|
234
|
+
},
|
|
235
|
+
"then": { "$ref": "#/$defs/textBlock" }
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"if": {
|
|
239
|
+
"properties": { "type": { "const": "formula" } },
|
|
240
|
+
"required": ["type"],
|
|
241
|
+
"type": "object"
|
|
242
|
+
},
|
|
243
|
+
"then": { "$ref": "#/$defs/formula" }
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"if": {
|
|
247
|
+
"properties": { "type": { "const": "figure" } },
|
|
248
|
+
"required": ["type"],
|
|
249
|
+
"type": "object"
|
|
250
|
+
},
|
|
251
|
+
"then": { "$ref": "#/$defs/figure" }
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"if": {
|
|
255
|
+
"properties": { "type": { "const": "labelled-figure" } },
|
|
256
|
+
"required": ["type"],
|
|
257
|
+
"type": "object"
|
|
258
|
+
},
|
|
259
|
+
"then": { "$ref": "#/$defs/labelledFigure" }
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"if": {
|
|
263
|
+
"properties": { "type": { "const": "table" } },
|
|
264
|
+
"required": ["type"],
|
|
265
|
+
"type": "object"
|
|
266
|
+
},
|
|
267
|
+
"then": { "$ref": "#/$defs/table" }
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"if": {
|
|
271
|
+
"properties": { "type": { "const": "quote" } },
|
|
272
|
+
"required": ["type"],
|
|
273
|
+
"type": "object"
|
|
274
|
+
},
|
|
275
|
+
"then": { "$ref": "#/$defs/quote" }
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"if": {
|
|
279
|
+
"properties": { "type": { "const": "code" } },
|
|
280
|
+
"required": ["type"],
|
|
281
|
+
"type": "object"
|
|
282
|
+
},
|
|
283
|
+
"then": { "$ref": "#/$defs/code" }
|
|
284
|
+
}
|
|
285
|
+
],
|
|
286
|
+
"unevaluatedProperties": false
|
|
287
|
+
},
|
|
288
|
+
"textBlock": {
|
|
289
|
+
"description": "Prose in restricted Markdown.",
|
|
290
|
+
"required": ["text"],
|
|
291
|
+
"properties": { "text": { "$ref": "urn:lernapps:schema:text.v1#/$defs/markdown" } },
|
|
292
|
+
"type": "object"
|
|
293
|
+
},
|
|
294
|
+
"formula": {
|
|
295
|
+
"description": "One formula on its own line, rendered at build to MathML.",
|
|
296
|
+
"required": ["formula"],
|
|
297
|
+
"properties": { "formula": { "$ref": "#/$defs/latex" } },
|
|
298
|
+
"type": "object"
|
|
299
|
+
},
|
|
300
|
+
"latex": {
|
|
301
|
+
"description": "One formula in the LaTeX subset of the formula command list: \\frac, \\cdot, \\times, \\div, \\%, \\text, ^, _, \\sqrt, \\approx, \\pm, \\le, \\ge, \\neq, \\, (thin space), parentheses and braces; \\ce{…} for chemical equations and \\pu{…} for units (mhchem). Written without the $ signs. A map by language only when it has \\text{…} that differs by language.",
|
|
302
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
303
|
+
},
|
|
304
|
+
"table": {
|
|
305
|
+
"description": "A table with a caption. Cells are restricted Markdown without paragraphs.",
|
|
306
|
+
"required": ["caption", "rows"],
|
|
307
|
+
"properties": {
|
|
308
|
+
"caption": {
|
|
309
|
+
"description": "What the table shows.",
|
|
310
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
311
|
+
},
|
|
312
|
+
"header": {
|
|
313
|
+
"description": "The header row: one heading per column.",
|
|
314
|
+
"type": "array",
|
|
315
|
+
"minItems": 1,
|
|
316
|
+
"items": { "$ref": "urn:lernapps:schema:text.v1#/$defs/markdown" }
|
|
317
|
+
},
|
|
318
|
+
"rowHeaders": {
|
|
319
|
+
"description": "The first cell of each row heads the row.",
|
|
320
|
+
"type": "boolean",
|
|
321
|
+
"default": false
|
|
322
|
+
},
|
|
323
|
+
"rows": {
|
|
324
|
+
"type": "array",
|
|
325
|
+
"minItems": 1,
|
|
326
|
+
"items": {
|
|
327
|
+
"type": "array",
|
|
328
|
+
"minItems": 1,
|
|
329
|
+
"items": { "$ref": "urn:lernapps:schema:text.v1#/$defs/markdown" }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
"type": "object"
|
|
334
|
+
},
|
|
335
|
+
"quote": {
|
|
336
|
+
"description": "A quotation, quoted exactly, with its origin, source and licence.",
|
|
337
|
+
"required": [
|
|
338
|
+
"text",
|
|
339
|
+
"origin",
|
|
340
|
+
"source",
|
|
341
|
+
"licence"
|
|
342
|
+
],
|
|
343
|
+
"allOf": [{ "$ref": "#/$defs/credit" }],
|
|
344
|
+
"properties": {
|
|
345
|
+
"text": {
|
|
346
|
+
"description": "The quotation itself, exactly as written.",
|
|
347
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/markdown"
|
|
348
|
+
},
|
|
349
|
+
"origin": {
|
|
350
|
+
"description": "Who said or wrote it, where and when.",
|
|
351
|
+
"type": "object",
|
|
352
|
+
"additionalProperties": false,
|
|
353
|
+
"required": ["author"],
|
|
354
|
+
"properties": {
|
|
355
|
+
"author": {
|
|
356
|
+
"description": "Who said or wrote it. A name is the same in every language: a string, or a map by language where it differs.",
|
|
357
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
358
|
+
},
|
|
359
|
+
"work": {
|
|
360
|
+
"description": "Where it was said or written: the work, as it is called; a string, or a map by language where its title is translated.",
|
|
361
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
362
|
+
},
|
|
363
|
+
"year": {
|
|
364
|
+
"anyOf": [{ "type": "integer" }, { "$ref": "urn:lernapps:schema:text.v1#/$defs/text" }],
|
|
365
|
+
"description": "When: a year, or a text such as um 500 v. Chr."
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
"type": "object"
|
|
371
|
+
},
|
|
372
|
+
"code": {
|
|
373
|
+
"description": "Program code, highlighted at build.",
|
|
374
|
+
"required": ["language", "code"],
|
|
375
|
+
"properties": {
|
|
376
|
+
"language": { "enum": [
|
|
377
|
+
"bash",
|
|
378
|
+
"c",
|
|
379
|
+
"css",
|
|
380
|
+
"html",
|
|
381
|
+
"java",
|
|
382
|
+
"javascript",
|
|
383
|
+
"json",
|
|
384
|
+
"plaintext",
|
|
385
|
+
"python",
|
|
386
|
+
"sql",
|
|
387
|
+
"typescript"
|
|
388
|
+
] },
|
|
389
|
+
"code": {
|
|
390
|
+
"description": "The code, as it is.",
|
|
391
|
+
"type": "string",
|
|
392
|
+
"minLength": 1
|
|
393
|
+
},
|
|
394
|
+
"caption": {
|
|
395
|
+
"description": "What the code does.",
|
|
396
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
"type": "object"
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
// runtime/shared/text/text.v1.schema.json
|
|
404
|
+
var text_v1_schema_default = {
|
|
405
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
406
|
+
$id: "urn:lernapps:schema:text.v1",
|
|
407
|
+
title: "A learner text",
|
|
408
|
+
description: "The texts of the content of an app: a text, and restricted Markdown, each a string while German is the only language of the app, else a map of texts by language. The schemas of the quiz, of the explainer and of the blocks they share refer to it.",
|
|
409
|
+
$defs: {
|
|
410
|
+
"text": {
|
|
411
|
+
"description": "A text for learners 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.",
|
|
412
|
+
"anyOf": [{
|
|
413
|
+
"type": "string",
|
|
414
|
+
"minLength": 1,
|
|
415
|
+
"pattern": "\\S"
|
|
416
|
+
}, {
|
|
417
|
+
"type": "object",
|
|
418
|
+
"minProperties": 1,
|
|
419
|
+
"propertyNames": { "pattern": "^[a-z]{2}$" },
|
|
420
|
+
"additionalProperties": {
|
|
421
|
+
"type": "string",
|
|
422
|
+
"minLength": 1,
|
|
423
|
+
"pattern": "\\S"
|
|
424
|
+
}
|
|
425
|
+
}]
|
|
426
|
+
},
|
|
427
|
+
"markdown": {
|
|
428
|
+
"description": "Restricted Markdown, as a text or a map of texts by language: paragraphs, **bold**, *italics*, lists, links and line breaks, formulas as $…$ in the formula command list. No headings (use a section block), no HTML, no images (use a figure block), no code (use a code block), no quotes (use a quote block), no tables (use a table block). A link to a page of the app starts with / and names it from the app's root: /<chapter>/<page>/#<section>.",
|
|
429
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
// runtime/shared/questions/question.v1.schema.json
|
|
434
|
+
var question_v1_schema_default = {
|
|
435
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
436
|
+
$id: "urn:lernapps:schema:question.v1",
|
|
437
|
+
title: "A question of a lernapps app",
|
|
438
|
+
description: "One question of the six types (single-choice, multiple-choice, true-false, number, ordering, matching) as the quiz's question bank and the explainer's exercises write it. Use #/$defs/question for one question; the schemas of the quiz and of the explainer refer to it.",
|
|
439
|
+
$defs: {
|
|
440
|
+
"question": {
|
|
441
|
+
"type": "object",
|
|
442
|
+
"required": [
|
|
443
|
+
"id",
|
|
444
|
+
"type",
|
|
445
|
+
"text",
|
|
446
|
+
"explanation",
|
|
447
|
+
"links"
|
|
448
|
+
],
|
|
449
|
+
"properties": {
|
|
450
|
+
"id": {
|
|
451
|
+
"description": "Stable name of the question, used in its address (#frage-<id>): lower-case letters, digits and hyphens. Keep it when the text changes, so links to the question keep working.",
|
|
452
|
+
"type": "string",
|
|
453
|
+
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
|
|
454
|
+
},
|
|
455
|
+
"type": {
|
|
456
|
+
"description": "The kind of question.",
|
|
457
|
+
"enum": [
|
|
458
|
+
"single-choice",
|
|
459
|
+
"multiple-choice",
|
|
460
|
+
"true-false",
|
|
461
|
+
"number",
|
|
462
|
+
"ordering",
|
|
463
|
+
"matching"
|
|
464
|
+
]
|
|
465
|
+
},
|
|
466
|
+
"text": {
|
|
467
|
+
"description": "The question, complete in itself.",
|
|
468
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
469
|
+
},
|
|
470
|
+
"material": {
|
|
471
|
+
"description": "Optional: content that the question shows, between its text and its answers: prose, formulas, figures and labelled figures (bundled images in src/images/, or drawn from data), tables, quotations and code. A figure has an alternative text and a caption, and an image its source and licence. A question that asks about what the content shows needs it; one that merely looks nicer with an image should not have one.",
|
|
472
|
+
"type": "array",
|
|
473
|
+
"minItems": 1,
|
|
474
|
+
"items": { "$ref": "urn:lernapps:schema:blocks.v1#/$defs/materialBlock" }
|
|
475
|
+
},
|
|
476
|
+
"explanation": {
|
|
477
|
+
"description": "Why the right answer is right, in one to three sentences. For number, ordering and matching questions also the background knowledge, as their options carry none.",
|
|
478
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
479
|
+
},
|
|
480
|
+
"links": {
|
|
481
|
+
"description": "Further reading on the question, at least one link, e.g. to the article in the German Wikipedia; in a quiz or on a page in several languages to the article in each language's Wikipedia, with url as a map by language.",
|
|
482
|
+
"type": "array",
|
|
483
|
+
"minItems": 1,
|
|
484
|
+
"items": { "$ref": "#/$defs/link" }
|
|
485
|
+
},
|
|
486
|
+
"hint": {
|
|
487
|
+
"description": "Optional: what to look at again after a wrong answer, in one sentence, without giving the answer away, e.g. \"Schau dir noch einmal an, wie hoch die Spannung im Haushalt ist.\" Shown with the feedback of a wrong answer when nothing more specific applies: a chosen option's info or a number's typical mistake come first. In an explainer, a wrong answer can be tried again, with this hint, before the solution shows.",
|
|
488
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
489
|
+
},
|
|
490
|
+
"examples": { "$ref": "#/$defs/examples" }
|
|
491
|
+
},
|
|
492
|
+
"allOf": [
|
|
493
|
+
{
|
|
494
|
+
"if": {
|
|
495
|
+
"type": "object",
|
|
496
|
+
"properties": { "type": { "const": "single-choice" } },
|
|
497
|
+
"required": ["type"]
|
|
498
|
+
},
|
|
499
|
+
"then": { "$ref": "#/$defs/choice" }
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
"if": {
|
|
503
|
+
"type": "object",
|
|
504
|
+
"properties": { "type": { "const": "multiple-choice" } },
|
|
505
|
+
"required": ["type"]
|
|
506
|
+
},
|
|
507
|
+
"then": { "$ref": "#/$defs/choice" }
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
"if": {
|
|
511
|
+
"type": "object",
|
|
512
|
+
"properties": { "type": { "const": "true-false" } },
|
|
513
|
+
"required": ["type"]
|
|
514
|
+
},
|
|
515
|
+
"then": { "$ref": "#/$defs/trueFalse" }
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
"if": {
|
|
519
|
+
"type": "object",
|
|
520
|
+
"properties": { "type": { "const": "number" } },
|
|
521
|
+
"required": ["type"]
|
|
522
|
+
},
|
|
523
|
+
"then": { "$ref": "#/$defs/number" }
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
"if": {
|
|
527
|
+
"type": "object",
|
|
528
|
+
"properties": { "type": { "const": "ordering" } },
|
|
529
|
+
"required": ["type"]
|
|
530
|
+
},
|
|
531
|
+
"then": { "$ref": "#/$defs/ordering" }
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
"if": {
|
|
535
|
+
"type": "object",
|
|
536
|
+
"properties": { "type": { "const": "matching" } },
|
|
537
|
+
"required": ["type"]
|
|
538
|
+
},
|
|
539
|
+
"then": { "$ref": "#/$defs/matching" }
|
|
540
|
+
}
|
|
541
|
+
],
|
|
542
|
+
"unevaluatedProperties": false
|
|
543
|
+
},
|
|
544
|
+
"examples": {
|
|
545
|
+
"description": "Optional: test cases for the build, never shown to learners. Before you write the question, note which answers fulfil its text and which typical wrong answers there are; write the other correct answers the text allows (also those other than the one you marked) into accepted, the typical wrong ones into rejected. The build runs each through the same check the page uses and fails when it accepts a rejected answer or rejects an accepted one: then the question checks something other than what its text asks (e.g. an ordering whose text says the order does not matter), so change the question or its text. An answer as the learner gives it, by the type: single-choice an option; multiple-choice a list of options, e.g. [0, 2]; true-false true or false; number a number, or the text as a learner types it, e.g. \"12,5\"; ordering every item in the learner's order; matching a list of pairs [left, right], one for each left entry. An option, item or entry is its index from 0 in the order written, or its text as written (in German). A number's mistakes and the info of wrong options already count as rejected.",
|
|
546
|
+
"type": "object",
|
|
547
|
+
"additionalProperties": false,
|
|
548
|
+
"minProperties": 1,
|
|
549
|
+
"properties": {
|
|
550
|
+
"accepted": {
|
|
551
|
+
"description": "Answers the text allows, each of which the check must accept.",
|
|
552
|
+
"type": "array",
|
|
553
|
+
"minItems": 1,
|
|
554
|
+
"items": { "$ref": "#/$defs/exampleAnswer" }
|
|
555
|
+
},
|
|
556
|
+
"rejected": {
|
|
557
|
+
"description": "Typical wrong answers, each of which the check must reject.",
|
|
558
|
+
"type": "array",
|
|
559
|
+
"minItems": 1,
|
|
560
|
+
"items": { "$ref": "#/$defs/exampleAnswer" }
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
"exampleAnswer": { "description": "An answer as the learner gives it, in the shape of the question's type (see examples); the build checks its shape." },
|
|
565
|
+
"choice": {
|
|
566
|
+
"type": "object",
|
|
567
|
+
"description": "Single choice: exactly one option is correct. Multiple choice: at least one option is correct, and the learner selects every correct one.",
|
|
568
|
+
"required": ["options"],
|
|
569
|
+
"properties": { "options": {
|
|
570
|
+
"type": "array",
|
|
571
|
+
"minItems": 2,
|
|
572
|
+
"maxItems": 8,
|
|
573
|
+
"items": {
|
|
574
|
+
"type": "object",
|
|
575
|
+
"additionalProperties": false,
|
|
576
|
+
"required": [
|
|
577
|
+
"text",
|
|
578
|
+
"correct",
|
|
579
|
+
"info"
|
|
580
|
+
],
|
|
581
|
+
"properties": {
|
|
582
|
+
"text": {
|
|
583
|
+
"description": "The option as the learner reads it.",
|
|
584
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
585
|
+
},
|
|
586
|
+
"correct": {
|
|
587
|
+
"description": "Whether this option is correct.",
|
|
588
|
+
"type": "boolean"
|
|
589
|
+
},
|
|
590
|
+
"info": {
|
|
591
|
+
"description": "One to three sentences of background on this option itself, true whether or not it is the right answer, e.g. \"Der Uluru ist ein Inselberg in der Wüste Zentralaustraliens ...\". After an answer it shows for every option. For a wrong option it is also the reason a learner who chose it reads with the feedback, so say there why it does not fit.",
|
|
592
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
593
|
+
},
|
|
594
|
+
"alt": {
|
|
595
|
+
"description": "Optional: the option as it is spoken, for the name of its radio button or checkbox, when its text is only a formula, e.g. \"zwei hoch sieben\" for \"$2^7$\". Without it the name is made from the formula in the page's language (\"2 hoch 7\"); write it where that reads badly, e.g. for a chemical formula. Not needed when the text has words.",
|
|
596
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
597
|
+
},
|
|
598
|
+
"link": {
|
|
599
|
+
"description": "Further reading on this option.",
|
|
600
|
+
"$ref": "#/$defs/link"
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
} }
|
|
605
|
+
},
|
|
606
|
+
"trueFalse": {
|
|
607
|
+
"type": "object",
|
|
608
|
+
"description": "A statement the learner judges as true or false; the background goes into the explanation.",
|
|
609
|
+
"required": ["answer"],
|
|
610
|
+
"properties": { "answer": {
|
|
611
|
+
"description": "Whether the statement is true.",
|
|
612
|
+
"type": "boolean"
|
|
613
|
+
} }
|
|
614
|
+
},
|
|
615
|
+
"number": {
|
|
616
|
+
"type": "object",
|
|
617
|
+
"description": "The learner types a number; it counts when it is within the tolerance of the answer. A comma or a point both work as the decimal sign. The background goes into the explanation.",
|
|
618
|
+
"required": ["answer", "tolerance"],
|
|
619
|
+
"properties": {
|
|
620
|
+
"answer": {
|
|
621
|
+
"description": "The right number.",
|
|
622
|
+
"type": "number"
|
|
623
|
+
},
|
|
624
|
+
"tolerance": {
|
|
625
|
+
"description": "How far the learner's number may be from the answer and still count; 0 for an exact answer. Match it to the rounding the question asks for.",
|
|
626
|
+
"type": "number",
|
|
627
|
+
"minimum": 0
|
|
628
|
+
},
|
|
629
|
+
"unit": {
|
|
630
|
+
"description": "The unit, shown next to the input, e.g. \"cm\"; the learner does not type it.",
|
|
631
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
632
|
+
},
|
|
633
|
+
"mistakes": {
|
|
634
|
+
"description": "Optional: typical wrong numbers, each with a message that says what went wrong, e.g. { \"answer\": 2300, \"message\": \"Du hast mit 10 statt mit 100 multipliziert.\" }. A learner's number within the tolerance of a mistake's answer gets its message with the feedback. A mistake's answer must not count as right, and each is listed once.",
|
|
635
|
+
"type": "array",
|
|
636
|
+
"minItems": 1,
|
|
637
|
+
"items": {
|
|
638
|
+
"type": "object",
|
|
639
|
+
"additionalProperties": false,
|
|
640
|
+
"required": ["answer", "message"],
|
|
641
|
+
"properties": {
|
|
642
|
+
"answer": {
|
|
643
|
+
"description": "The wrong number, as the mistake gives it.",
|
|
644
|
+
"type": "number"
|
|
645
|
+
},
|
|
646
|
+
"message": {
|
|
647
|
+
"description": "What went wrong, in one or two sentences, without giving the right number away.",
|
|
648
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
"ordering": {
|
|
656
|
+
"type": "object",
|
|
657
|
+
"description": "The learner puts the items in order, e.g. by height. Write them in the right order: the page shows them mixed. The background goes into the explanation.",
|
|
658
|
+
"required": ["items"],
|
|
659
|
+
"properties": { "items": {
|
|
660
|
+
"type": "array",
|
|
661
|
+
"minItems": 2,
|
|
662
|
+
"maxItems": 8,
|
|
663
|
+
"items": { "$ref": "urn:lernapps:schema:text.v1#/$defs/text" }
|
|
664
|
+
} }
|
|
665
|
+
},
|
|
666
|
+
"matching": {
|
|
667
|
+
"type": "object",
|
|
668
|
+
"description": "The learner matches each left entry with its right entry. Write the pairs as they belong together: the page shows the right entries mixed. The background goes into the explanation. Instead of the pairs, a question about a figure to label takes them from the figure (pairsFrom).",
|
|
669
|
+
"properties": {
|
|
670
|
+
"pairsFrom": {
|
|
671
|
+
"description": "material: the pairs are the labels of the labelled figure in the material, each marker (A, B, 1) on the left with its text on the right, so that they are written once. The figure must have legend: false, as its legend would give the answers away; the solution lists them. Write either the pairs or pairsFrom.",
|
|
672
|
+
"const": "material"
|
|
673
|
+
},
|
|
674
|
+
"distractors": {
|
|
675
|
+
"description": "Optional: right entries that match none of the left entries, wrong answers the learner may choose, shown mixed with the right entries. Use them for plausible mistakes, or for entries that have nothing to do with the task; 1 to 4 of them.",
|
|
676
|
+
"type": "array",
|
|
677
|
+
"minItems": 1,
|
|
678
|
+
"maxItems": 4,
|
|
679
|
+
"items": { "$ref": "urn:lernapps:schema:text.v1#/$defs/text" }
|
|
680
|
+
},
|
|
681
|
+
"pairs": {
|
|
682
|
+
"description": "The pairs, written out. Without pairsFrom they are required.",
|
|
683
|
+
"type": "array",
|
|
684
|
+
"minItems": 2,
|
|
685
|
+
"maxItems": 8,
|
|
686
|
+
"items": {
|
|
687
|
+
"type": "object",
|
|
688
|
+
"additionalProperties": false,
|
|
689
|
+
"required": ["left", "right"],
|
|
690
|
+
"properties": {
|
|
691
|
+
"left": { "$ref": "urn:lernapps:schema:text.v1#/$defs/text" },
|
|
692
|
+
"right": { "$ref": "urn:lernapps:schema:text.v1#/$defs/text" }
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
"link": {
|
|
699
|
+
"type": "object",
|
|
700
|
+
"additionalProperties": false,
|
|
701
|
+
"required": ["title", "url"],
|
|
702
|
+
"properties": {
|
|
703
|
+
"title": {
|
|
704
|
+
"description": "The link's text: what the learner finds there, e.g. \"Uluru (Wikipedia)\".",
|
|
705
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
706
|
+
},
|
|
707
|
+
"url": {
|
|
708
|
+
"description": "The address, https only: one for every language, or, in a quiz or on a page in several languages where the page to read differs by language (the article in each language's Wikipedia), a map of addresses by language ({ \"de\": \"https://de.wikipedia.org/wiki/Uluru\", \"en\": \"https://en.wikipedia.org/wiki/Uluru\" }) with every declared language, as a text's map. Each language's page links the address of its language. Check that each opens the page you mean.",
|
|
709
|
+
"$ref": "#/$defs/address"
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
"address": {
|
|
714
|
+
"description": "An address, https only: a string, the same in every language, or a map of addresses by language ({ de: …, en: … }) in a quiz or on a page in several languages. The quiz or page checks that a map has every language it declares, and no other.",
|
|
715
|
+
"anyOf": [{
|
|
716
|
+
"type": "string",
|
|
717
|
+
"pattern": "^https://[^\\s]+$"
|
|
718
|
+
}, {
|
|
719
|
+
"type": "object",
|
|
720
|
+
"minProperties": 1,
|
|
721
|
+
"propertyNames": { "pattern": "^[a-z]{2}$" },
|
|
722
|
+
"additionalProperties": {
|
|
723
|
+
"type": "string",
|
|
724
|
+
"pattern": "^https://[^\\s]+$"
|
|
725
|
+
}
|
|
726
|
+
}]
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
// runtime/shared/questions/validate.ts
|
|
731
|
+
/** The id of the schema of a question, to refer to it from another schema (with `questionSchemas` added to its validator). */
|
|
732
|
+
const QUESTION_SCHEMA_ID = question_v1_schema_default.$id;
|
|
733
|
+
/** The reference to one question in the schema of a question, for a `$ref` from another schema. */
|
|
734
|
+
const QUESTION_REF = `${QUESTION_SCHEMA_ID}#/$defs/question`;
|
|
735
|
+
/** The schema of a question (question.v1.schema.json), to add to another schema's validator. */
|
|
736
|
+
const questionSchema = question_v1_schema_default;
|
|
737
|
+
/** The schemas a question refers to, and its own: every validator of a schema that refers to a question adds them. */
|
|
738
|
+
const questionSchemas = [
|
|
739
|
+
text_v1_schema_default,
|
|
740
|
+
blocks_v1_schema_default,
|
|
741
|
+
question_v1_schema_default
|
|
742
|
+
];
|
|
743
|
+
const schemas = schemasById(...questionSchemas);
|
|
744
|
+
/** The definition of the fields of each type of question in its schema. */
|
|
745
|
+
const QUESTION_BRANCH = {
|
|
746
|
+
"single-choice": "choice",
|
|
747
|
+
"multiple-choice": "choice",
|
|
748
|
+
"true-false": "trueFalse",
|
|
749
|
+
number: "number",
|
|
750
|
+
ordering: "ordering",
|
|
751
|
+
matching: "matching"
|
|
752
|
+
};
|
|
753
|
+
const ajv = new Ajv2020({
|
|
754
|
+
allErrors: true,
|
|
755
|
+
strict: true
|
|
756
|
+
});
|
|
757
|
+
for (const each of questionSchemas) ajv.addSchema(each);
|
|
758
|
+
const validateOne = (() => {
|
|
759
|
+
const validate = ajv.getSchema(QUESTION_REF);
|
|
760
|
+
if (validate === void 0) throw new Error(`the question's schema has no ${QUESTION_REF}`);
|
|
761
|
+
return validate;
|
|
762
|
+
})();
|
|
763
|
+
/** `["options", "1", "text"]` as `.options[1].text`. */
|
|
764
|
+
const readable = (parts) => parts.map((part) => /^\d+$/.test(part) ? `[${part}]` : `.${part}`).join("");
|
|
765
|
+
/** Where something in a question is: the question at `at`, the path below it, and the question's id if it has one. */
|
|
766
|
+
function inQuestion(at, parts, question) {
|
|
767
|
+
const id = typeof question === "object" && question !== null && "id" in question && typeof question.id === "string" ? question.id : void 0;
|
|
768
|
+
return id === void 0 ? `${at}${readable(parts)}` : `${at}${readable(parts)} (question "${id}")`;
|
|
769
|
+
}
|
|
770
|
+
/** `/questions/2/options/1/text` as `questions[2].options[1].text`, with the id of the question. */
|
|
771
|
+
function valueAt(root, pointer) {
|
|
772
|
+
return pointer.split("/").slice(1).reduce((at, part) => typeof at === "object" && at !== null ? at[part] : void 0, root);
|
|
773
|
+
}
|
|
774
|
+
/** A learner text that is neither a text nor a map of texts. */
|
|
775
|
+
function textMessage(value, at) {
|
|
776
|
+
if (typeof value === "string") return `${at}: must not be empty`;
|
|
777
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
778
|
+
const bad = Object.entries(value).find(([lang, text]) => !/^[a-z]{2}$/.test(lang) || typeof text !== "string" || text.trim() === "");
|
|
779
|
+
if (bad !== void 0) return /^[a-z]{2}$/.test(bad[0]) ? `${at} (${bad[0]}): must not be empty` : `${at}: ${bad[0]} is not a language code; use two letters, e.g. de, en`;
|
|
780
|
+
}
|
|
781
|
+
return `${at}: must be a text, or a map of texts by language: { de: …, en: … }`;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* What is wrong with a link's address that is neither an https address nor a map of them by language: in which
|
|
785
|
+
* language, when it is one of a map. Also for the explainer, which embeds the quiz's question types.
|
|
786
|
+
*/
|
|
787
|
+
function addressProblem(value) {
|
|
788
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) for (const [lang, address] of Object.entries(value)) {
|
|
789
|
+
if (!/^[a-z]{2}$/.test(lang)) return { message: `${lang} is not a language code; use two letters, e.g. de, en` };
|
|
790
|
+
if (typeof address !== "string" || !/^https:\/\/\S+$/.test(address)) return {
|
|
791
|
+
lang,
|
|
792
|
+
message: "must be an https address"
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
return { message: "must be an https address, or a map of https addresses by language: { de: https://…, en: https://… }" };
|
|
796
|
+
}
|
|
797
|
+
function schemaMessage(error, at) {
|
|
798
|
+
switch (error.keyword) {
|
|
799
|
+
case "if": return;
|
|
800
|
+
case "required": return `${at}: ${String(error.params["missingProperty"])} is missing`;
|
|
801
|
+
case "additionalProperties": return `${at}: ${String(error.params["additionalProperty"])} is not allowed here`;
|
|
802
|
+
case "unevaluatedProperties": return `${at}: ${String(error.params["unevaluatedProperty"])} is not allowed ${/\/material\/\d+$/.test(error.instancePath) ? "in this type of block" : "for this type of question"}`;
|
|
803
|
+
case "enum": return `${at}: must be one of ${error.params["allowedValues"].join(", ")}`;
|
|
804
|
+
case "pattern": return error.instancePath.endsWith("/id") ? `${at}: use lower-case letters, digits and hyphens, e.g. "brueche-kuerzen"` : `${at}: must not be empty`;
|
|
805
|
+
default: return `${at}: ${error.message ?? "is invalid"}`;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/** The messages of the schema's errors, each at the place `where` names for its path. */
|
|
809
|
+
function schemaMessages(errors, where, root) {
|
|
810
|
+
const texts = new Set(errors.filter((error) => error.keyword === "anyOf").map((error) => error.instancePath));
|
|
811
|
+
const underText = (path) => [...texts].some((text) => path === text || path.startsWith(`${text}/`));
|
|
812
|
+
const unknownType = new Set(errors.filter((error) => error.keyword === "enum" && error.instancePath.endsWith("/type")).map((error) => error.instancePath.slice(0, -5)));
|
|
813
|
+
const isKnown = (error) => {
|
|
814
|
+
if (error.keyword !== "unevaluatedProperties") return false;
|
|
815
|
+
const value = valueAt(root, error.instancePath);
|
|
816
|
+
const type = typeof value === "object" && value !== null && "type" in value ? String(value.type) : "";
|
|
817
|
+
const property = String(error.params["unevaluatedProperty"]);
|
|
818
|
+
const material = /\/material\/\d+$/.test(error.instancePath) ? MATERIAL_DEFINITIONS[type] : void 0;
|
|
819
|
+
if (material !== void 0) return knownProperties(schemas, blocks_v1_schema_default.$id, material).has(property);
|
|
820
|
+
const branch = QUESTION_BRANCH[type];
|
|
821
|
+
return branch !== void 0 && (knownProperties(schemas, question_v1_schema_default.$id, "question", { branches: false }).has(property) || knownProperties(schemas, question_v1_schema_default.$id, branch).has(property));
|
|
822
|
+
};
|
|
823
|
+
return errors.filter((error) => !(error.keyword === "unevaluatedProperties" && (unknownType.has(error.instancePath) || isKnown(error)))).filter((error) => error.keyword === "anyOf" || !underText(error.instancePath)).flatMap((error) => {
|
|
824
|
+
if (error.keyword !== "anyOf") return schemaMessage(error, where(error.instancePath)) ?? [];
|
|
825
|
+
const value = valueAt(root, error.instancePath);
|
|
826
|
+
if (!error.instancePath.endsWith("/url")) return textMessage(value, where(error.instancePath));
|
|
827
|
+
const { lang, message } = addressProblem(value);
|
|
828
|
+
return `${where(error.instancePath)}${lang === void 0 ? "" : ` (${lang})`}: ${message}`;
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
function duplicates(values) {
|
|
832
|
+
return [...new Set(values.filter((value, index) => values.indexOf(value) !== index))];
|
|
833
|
+
}
|
|
834
|
+
/** Where the pairs of a matching question come from: written, or taken from the labelled figure of its material. */
|
|
835
|
+
function pairsSourceProblems(question) {
|
|
836
|
+
const written = question.pairs !== void 0;
|
|
837
|
+
if (question.pairsFrom === void 0) return written ? [] : [{
|
|
838
|
+
path: ["pairs"],
|
|
839
|
+
message: "pairs is missing; write the pairs, or take them from the labelled figure of the material with pairsFrom: material"
|
|
840
|
+
}];
|
|
841
|
+
if (written) return [{
|
|
842
|
+
path: ["pairsFrom"],
|
|
843
|
+
message: "pairs and pairsFrom: write the pairs, or take them from the material, not both"
|
|
844
|
+
}];
|
|
845
|
+
const index = question.material?.findIndex((block) => block.type === "labelled-figure") ?? -1;
|
|
846
|
+
const figure = question.material?.[index];
|
|
847
|
+
if (figure?.type !== "labelled-figure") return [{
|
|
848
|
+
path: ["pairsFrom"],
|
|
849
|
+
message: "the material has no labelled figure; add one, or write the pairs"
|
|
850
|
+
}];
|
|
851
|
+
const problems = [];
|
|
852
|
+
if (figure.legend !== false) problems.push({
|
|
853
|
+
path: [
|
|
854
|
+
"material",
|
|
855
|
+
index,
|
|
856
|
+
"legend"
|
|
857
|
+
],
|
|
858
|
+
message: "the legend of the figure lists the answers before the question is answered; write legend: false, the solution shows them"
|
|
859
|
+
});
|
|
860
|
+
if (figure.labels.length < 2 || figure.labels.length > 8) problems.push({
|
|
861
|
+
path: ["pairsFrom"],
|
|
862
|
+
message: `the figure has ${figure.labels.length} labels; a matching question takes 2 to 8 pairs`
|
|
863
|
+
});
|
|
864
|
+
return problems;
|
|
865
|
+
}
|
|
866
|
+
/** What the schema cannot say about one question, its texts in German. */
|
|
867
|
+
function questionProblems(question) {
|
|
868
|
+
const problems = [];
|
|
869
|
+
const twice = (what, path, values) => {
|
|
870
|
+
for (const value of duplicates(values)) problems.push({
|
|
871
|
+
path: [path],
|
|
872
|
+
message: `the ${what} "${value}" appears twice`
|
|
873
|
+
});
|
|
874
|
+
};
|
|
875
|
+
switch (question.type) {
|
|
876
|
+
case "single-choice": {
|
|
877
|
+
const correct = question.options.filter((option) => option.correct).length;
|
|
878
|
+
if (correct !== 1) problems.push({
|
|
879
|
+
path: ["options"],
|
|
880
|
+
message: `a single-choice question needs exactly one correct option; it has ${correct}. Mark one option "correct": true, or make it "multiple-choice"`
|
|
881
|
+
});
|
|
882
|
+
twice("option", "options", question.options.map((option) => option.text));
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
case "multiple-choice":
|
|
886
|
+
if (!question.options.some((option) => option.correct)) problems.push({
|
|
887
|
+
path: ["options"],
|
|
888
|
+
message: "a multiple-choice question needs at least one correct option; it has none"
|
|
889
|
+
});
|
|
890
|
+
twice("option", "options", question.options.map((option) => option.text));
|
|
891
|
+
break;
|
|
892
|
+
case "ordering":
|
|
893
|
+
twice("item", "items", question.items);
|
|
894
|
+
break;
|
|
895
|
+
case "matching":
|
|
896
|
+
twice("left entry", "pairs", question.pairs.map((pair) => pair.left));
|
|
897
|
+
twice("right entry", "pairs", rightEntries(question));
|
|
898
|
+
break;
|
|
899
|
+
case "number":
|
|
900
|
+
question.mistakes?.forEach((mistake, index) => {
|
|
901
|
+
if (Math.abs(mistake.answer - question.answer) <= question.tolerance + 1e-9) problems.push({
|
|
902
|
+
path: [
|
|
903
|
+
"mistakes",
|
|
904
|
+
index,
|
|
905
|
+
"answer"
|
|
906
|
+
],
|
|
907
|
+
message: `the mistake ${mistake.answer} counts as right: it is within the tolerance of the answer ${question.answer}; give the wrong number the mistake leads to`
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
for (const value of duplicates((question.mistakes ?? []).map((mistake) => String(mistake.answer)))) problems.push({
|
|
911
|
+
path: ["mistakes"],
|
|
912
|
+
message: `the mistake ${value} appears twice; list each wrong number once`
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
return problems;
|
|
916
|
+
}
|
|
917
|
+
/** The index of an option, item or entry given as its index from 0 or as its text in German; or what is wrong. */
|
|
918
|
+
function indexOf(value, texts, what) {
|
|
919
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value < texts.length) return value;
|
|
920
|
+
if (typeof value === "string" && texts.includes(value)) return texts.indexOf(value);
|
|
921
|
+
return `${typeof value === "string" ? `"${value}"` : JSON.stringify(value)} is not ${what}; give its index from 0 to ${texts.length - 1}, or its text as written (in German)`;
|
|
922
|
+
}
|
|
923
|
+
/** Indices of `values`, each in `texts`; or the first problem. */
|
|
924
|
+
function indicesOf(values, texts, what) {
|
|
925
|
+
const found = values.map((value) => indexOf(value, texts, what));
|
|
926
|
+
const problem = found.find((value) => typeof value === "string");
|
|
927
|
+
return typeof problem === "string" ? problem : found;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* An example as the checker receives a learner's answer to `question` (in German): single choice an option, multiple
|
|
931
|
+
* choice a list of options, true/false a boolean, number a number or the text a learner types, ordering the items in
|
|
932
|
+
* the learner's order, matching a list of pairs [left, right]; an option, item or entry by its index from 0 or its
|
|
933
|
+
* text. A complete answer, as the page only checks one.
|
|
934
|
+
*/
|
|
935
|
+
function exampleAnswer(question, example) {
|
|
936
|
+
const expect = (what) => ({ problem: `give ${what}` });
|
|
937
|
+
let answer;
|
|
938
|
+
switch (question.type) {
|
|
939
|
+
case "single-choice": {
|
|
940
|
+
const index = indexOf(example, question.options.map((option) => option.text), "an option");
|
|
941
|
+
if (typeof index === "string") return { problem: index };
|
|
942
|
+
answer = index;
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
case "multiple-choice": {
|
|
946
|
+
if (!Array.isArray(example)) return expect("the chosen options as a list: [0, 2]");
|
|
947
|
+
const indices = indicesOf(example, question.options.map((option) => option.text), "an option");
|
|
948
|
+
if (typeof indices === "string") return { problem: indices };
|
|
949
|
+
answer = [...new Set(indices)].sort((a, b) => a - b);
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
case "true-false":
|
|
953
|
+
if (typeof example !== "boolean") return expect("true or false");
|
|
954
|
+
answer = example;
|
|
955
|
+
break;
|
|
956
|
+
case "number":
|
|
957
|
+
if (typeof example === "number") answer = String(example);
|
|
958
|
+
else if (typeof example === "string" && parseNumber(example, question.unit) !== void 0) answer = example;
|
|
959
|
+
else return expect("a number, or the text a learner types, e.g. \"12,5\"");
|
|
960
|
+
break;
|
|
961
|
+
case "ordering": {
|
|
962
|
+
if (!Array.isArray(example)) return expect("the items in the learner's order as a list");
|
|
963
|
+
const order = indicesOf(example, question.items, "an item");
|
|
964
|
+
if (typeof order === "string") return { problem: order };
|
|
965
|
+
if (new Set(order).size !== question.items.length || order.length !== question.items.length) return expect(`each of the ${question.items.length} items once, in the learner's order`);
|
|
966
|
+
answer = question.items.map((_item, index) => order.indexOf(index));
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
case "matching": {
|
|
970
|
+
const pairs = Array.isArray(example) ? example : [];
|
|
971
|
+
if (!Array.isArray(example) || pairs.some((pair) => !Array.isArray(pair) || pair.length !== 2)) return expect("a list of pairs [left, right]");
|
|
972
|
+
const lefts = indicesOf(pairs.map((pair) => pair[0]), question.pairs.map((pair) => pair.left), "a left entry");
|
|
973
|
+
if (typeof lefts === "string") return { problem: lefts };
|
|
974
|
+
const rights = indicesOf(pairs.map((pair) => pair[1]), rightEntries(question), "a right entry");
|
|
975
|
+
if (typeof rights === "string") return { problem: rights };
|
|
976
|
+
if (new Set(lefts).size !== question.pairs.length || lefts.length !== question.pairs.length) return expect(`one pair for each of the ${question.pairs.length} left entries`);
|
|
977
|
+
answer = question.pairs.map((_pair, index) => rights[lefts.indexOf(index)] ?? -1);
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return isComplete(question, answer) ? { answer } : expect("a complete answer: the page checks only those");
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Whether each example of `question` (in German) gets the verdict it is listed with from the same check the page
|
|
985
|
+
* uses: an accepted one passes, a rejected one fails. Each problem at the example's place.
|
|
986
|
+
*/
|
|
987
|
+
function exampleProblems(question, examples) {
|
|
988
|
+
const problems = [];
|
|
989
|
+
for (const kind of ["accepted", "rejected"]) (examples?.[kind] ?? []).forEach((example, index) => {
|
|
990
|
+
const path = [
|
|
991
|
+
"examples",
|
|
992
|
+
kind,
|
|
993
|
+
index
|
|
994
|
+
];
|
|
995
|
+
const given = exampleAnswer(question, example);
|
|
996
|
+
if ("problem" in given) {
|
|
997
|
+
problems.push({
|
|
998
|
+
path,
|
|
999
|
+
message: given.problem
|
|
1000
|
+
});
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const right = isCorrect(question, given.answer);
|
|
1004
|
+
if (right === (kind === "accepted")) return;
|
|
1005
|
+
problems.push({
|
|
1006
|
+
path,
|
|
1007
|
+
message: verdict(JSON.stringify(example), right, "the check", "change the question (e.g. another type, a tolerance) so that its check")
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
return problems;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* The problems of a question that matches the schema, with their place in it: its texts complete in each of
|
|
1014
|
+
* `languages` and in no other (German first, with the messages of the explainer's language check), so its addresses
|
|
1015
|
+
* given as maps, then what the schema cannot say. A plain string is German, complete only while German is the only
|
|
1016
|
+
* language; a plain address is the same in every language.
|
|
1017
|
+
*/
|
|
1018
|
+
function questionDetails(question, languages = ["de"], declaredIn, app) {
|
|
1019
|
+
const problems = [];
|
|
1020
|
+
if (question.material !== void 0) {
|
|
1021
|
+
const found = checkMaterial(question.material, {
|
|
1022
|
+
languages,
|
|
1023
|
+
...declaredIn === void 0 ? {} : { declaredIn },
|
|
1024
|
+
imageExists: app?.imageExists ?? (() => false),
|
|
1025
|
+
...app?.onLink === void 0 ? {} : { onLink: app.onLink }
|
|
1026
|
+
});
|
|
1027
|
+
for (const problem of found) problems.push({
|
|
1028
|
+
...problem,
|
|
1029
|
+
path: ["material", ...problem.path]
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
if (question.type === "matching") {
|
|
1033
|
+
const source = pairsSourceProblems(question);
|
|
1034
|
+
if (source.length > 0) return [...problems, ...source];
|
|
1035
|
+
}
|
|
1036
|
+
for (const { path, text } of questionTexts(question)) for (const problem of textLanguages(text, languages, false, declaredIn).problems) problems.push({
|
|
1037
|
+
path,
|
|
1038
|
+
lang: problem.lang,
|
|
1039
|
+
message: problem.message
|
|
1040
|
+
});
|
|
1041
|
+
for (const { path, text } of questionAddresses(question)) for (const problem of textLanguages(text, languages, true, declaredIn).problems) problems.push({
|
|
1042
|
+
path,
|
|
1043
|
+
lang: problem.lang,
|
|
1044
|
+
message: problem.message
|
|
1045
|
+
});
|
|
1046
|
+
const german = questionIn(question, "de");
|
|
1047
|
+
const own = questionProblems(german).map((problem) => question.type === "matching" && question.pairsFrom !== void 0 && problem.path[0] === "pairs" ? {
|
|
1048
|
+
...problem,
|
|
1049
|
+
path: ["pairsFrom"]
|
|
1050
|
+
} : problem);
|
|
1051
|
+
return [
|
|
1052
|
+
...problems,
|
|
1053
|
+
...own,
|
|
1054
|
+
...exampleProblems(german, question.examples)
|
|
1055
|
+
];
|
|
1056
|
+
}
|
|
1057
|
+
/** Whether `value` matches the schema of one question; its errors as messages at `at` otherwise. */
|
|
1058
|
+
function questionSchemaProblems(value, at) {
|
|
1059
|
+
if (validateOne(value)) return [];
|
|
1060
|
+
return schemaMessages(validateOne.errors ?? [], (path) => inQuestion(at, path.split("/").slice(1), value), value);
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* The problems of one question; none when it is valid. `at` is where the question is, e.g. `questions[2]` in a bank
|
|
1064
|
+
* or `blocks[3]` on a page; each problem starts with it, the path below it and the question's id. `languages` are the
|
|
1065
|
+
* declared languages of the page, declared in `declaredIn` (src/app.yaml by default).
|
|
1066
|
+
*/
|
|
1067
|
+
function validateQuestion(value, at, languages = ["de"], declaredIn, app) {
|
|
1068
|
+
const schemaProblems = questionSchemaProblems(value, at);
|
|
1069
|
+
if (schemaProblems.length > 0) return schemaProblems;
|
|
1070
|
+
const question = value;
|
|
1071
|
+
return questionDetails(question, languages, declaredIn, app).map((problem) => {
|
|
1072
|
+
const path = problem.path.at(-1) === problem.lang ? problem.path.slice(0, -1) : problem.path;
|
|
1073
|
+
return problem.lang === void 0 && problem.path[0] !== "examples" && problem.path[0] !== "material" ? `${at} (question "${question.id}"): ${problem.message}` : `${at}${readable(path.map(String))}${problem.lang === void 0 ? "" : ` (${problem.lang})`} (question "${question.id}"): ${problem.message}`;
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
export { verdict as _, duplicates as a, questionSchema as c, readable as d, schemaMessages as f, schemasById as g, knownProperties as h, addressProblem as i, questionSchemaProblems as l, question_v1_schema_default as m, QUESTION_REF as n, inQuestion as o, validateQuestion as p, QUESTION_SCHEMA_ID as r, questionDetails as s, QUESTION_BRANCH as t, questionSchemas as u };
|