@lernapps/app-templates 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +195 -2
- package/dist/appname-Bh23cUuz.mjs +12 -0
- package/dist/assessment-By2GGTJC.mjs +1696 -0
- package/dist/block-BK_ohsH1.mjs +82 -0
- package/dist/blocks-BmRrD7UY.mjs +149 -0
- package/dist/blocks.mjs +4 -0
- package/dist/check-BkEl-m-F.mjs +254 -0
- package/dist/content-cYBPYg_J.mjs +140 -0
- package/dist/contract-PONpxJtd.mjs +52 -0
- package/dist/e2e-BiMI-pCL.mjs +89 -0
- package/dist/engine-BqF2Vffn.mjs +136 -0
- package/dist/explainer-e2e.mjs +1297 -0
- package/dist/explainer-islands.mjs +197 -0
- package/dist/explainer-plugin.mjs +2131 -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-C-0hUaSC.mjs +16947 -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-BKc8JYtC.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-BJey0bwl.mjs +540 -0
- package/dist/render-BL6fV_NI.mjs +507 -0
- package/dist/render-DpRO_RKm.mjs +146 -0
- package/dist/result-_eQcamoz.mjs +180 -0
- package/dist/tasks-DADUYy52.mjs +74 -0
- package/dist/validate-COaZso7u.mjs +1072 -0
- package/dist/validate-CxtdeQ5S.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 +142 -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 +446 -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 +1474 -0
- package/runtime/explainer/exercises.ts +324 -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 +299 -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 +430 -0
- package/runtime/shared/questions/question.v1.schema.json +342 -0
- package/runtime/shared/questions/questions.css +192 -0
- package/runtime/shared/questions/render.ts +247 -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/text.v1.schema.json +34 -0
- package/runtime/shared/vue-code.ts +395 -0
- package/runtime/shared/vue.ts +38 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
// The checks of one page of an explainer's content at build time, what the schema cannot say: its events, its images,
|
|
2
|
+
// its blocks with their texts, formulas, links and depth, its exercises (a question of the question types, a generated
|
|
3
|
+
// exercise with its expression and its property test, a vue-component block with its component), and that its exercises
|
|
4
|
+
// have their anchors once.
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { questionTexts } from "../shared/questions/question.ts";
|
|
8
|
+
import { questionDetails } from "../shared/questions/validate.ts";
|
|
9
|
+
import { checkMaterial, type AppChecks, type MaterialBlock } from "../shared/blocks/index.ts";
|
|
10
|
+
import { propsProblems, textPaths, withData } from "./components/props.ts";
|
|
11
|
+
import { readComponents } from "./components/read.ts";
|
|
12
|
+
import {
|
|
13
|
+
inLanguage,
|
|
14
|
+
termId,
|
|
15
|
+
type Block,
|
|
16
|
+
type BlockOf,
|
|
17
|
+
type ComponentUse,
|
|
18
|
+
type DataRecord,
|
|
19
|
+
type Event,
|
|
20
|
+
type Text,
|
|
21
|
+
} from "./content.ts";
|
|
22
|
+
import { ExpressionError, parse, variables } from "./expression.ts";
|
|
23
|
+
import { dataOf, templatesOf } from "./exercises.ts";
|
|
24
|
+
import { exampleProblems, soundness } from "./generated/soundness.ts";
|
|
25
|
+
import { templateProblems, unusedVariables } from "./generated/texts.ts";
|
|
26
|
+
import { type Generated, type Templates } from "./generated/types.ts";
|
|
27
|
+
import { validators } from "./schema.ts";
|
|
28
|
+
import { RESERVED_PARAMS } from "./tasks.ts";
|
|
29
|
+
import {
|
|
30
|
+
get,
|
|
31
|
+
isObject,
|
|
32
|
+
lineOf,
|
|
33
|
+
pathText,
|
|
34
|
+
schemaProblems,
|
|
35
|
+
type Path,
|
|
36
|
+
type Problems,
|
|
37
|
+
type Texts,
|
|
38
|
+
type YamlFile,
|
|
39
|
+
} from "../shared/content.ts";
|
|
40
|
+
import { textLanguages } from "../shared/languages.ts";
|
|
41
|
+
import { formulaProblems } from "../shared/text/markdown.ts";
|
|
42
|
+
|
|
43
|
+
/** A date of an event as a number to sort by: year, month, day. */
|
|
44
|
+
function dateOrder(date: number | string): number {
|
|
45
|
+
if (typeof date === "number") return date * 10_000;
|
|
46
|
+
const match = /^(-?\d{1,4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(date);
|
|
47
|
+
if (match === null) return 0;
|
|
48
|
+
const year = Number(match[1]);
|
|
49
|
+
return year * 10_000 + Number(match[2] ?? 0) * 100 + Number(match[3] ?? 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PageCheck {
|
|
53
|
+
yaml: YamlFile;
|
|
54
|
+
texts: Texts;
|
|
55
|
+
problems: Problems;
|
|
56
|
+
root: string;
|
|
57
|
+
dataFiles: ReadonlyMap<string, YamlFile>;
|
|
58
|
+
terms: Map<string, string>;
|
|
59
|
+
images: Map<string, string>;
|
|
60
|
+
/** The anchors of the page's sections: warum, regel/merkhilfe. */
|
|
61
|
+
anchors: Set<string>;
|
|
62
|
+
/** The ids of the page's exercises, which are anchors too, with where each is. */
|
|
63
|
+
exercises: Map<string, Path>;
|
|
64
|
+
/** How many seeds the property test of a generated exercise draws. */
|
|
65
|
+
seeds: number;
|
|
66
|
+
/** The components of src/components/, when it exists. */
|
|
67
|
+
components: ReturnType<typeof readComponents> | undefined;
|
|
68
|
+
/** The vue-component blocks so far. */
|
|
69
|
+
uses: ComponentUse[];
|
|
70
|
+
/** The data files that a timeline, a generated exercise or a component prop names so far, over all pages. */
|
|
71
|
+
dataUsed: Set<string>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The names of the data files that `value` gives as { data: <name> }, at any depth. */
|
|
75
|
+
function dataNames(value: unknown): string[] {
|
|
76
|
+
if (Array.isArray(value)) return value.flatMap(dataNames);
|
|
77
|
+
if (!isObject(value)) return [];
|
|
78
|
+
const keys = Object.keys(value);
|
|
79
|
+
if (keys.length === 1 && keys[0] === "data" && typeof value["data"] === "string") return [value["data"]];
|
|
80
|
+
return Object.values(value).flatMap(dataNames);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function checkEvents(check: PageCheck, yaml: YamlFile, path: Path, events: readonly Event[]): void {
|
|
84
|
+
events.forEach((event, index) => {
|
|
85
|
+
const at = [...path, index];
|
|
86
|
+
check.texts.check(yaml, [...at, "label"], "plain");
|
|
87
|
+
check.texts.check(yaml, [...at, "title"], "plain");
|
|
88
|
+
check.texts.check(yaml, [...at, "text"], "markdown");
|
|
89
|
+
const before = events[index - 1];
|
|
90
|
+
if (before !== undefined && dateOrder(event.date) < dateOrder(before.date)) {
|
|
91
|
+
check.problems.at(
|
|
92
|
+
yaml,
|
|
93
|
+
at,
|
|
94
|
+
`${event.date} comes before ${before.date}, the event above it; list the events from the earliest`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** What the checks of blocks that show content need from the app: its images, and where a link to a page goes. */
|
|
101
|
+
function appChecks(check: PageCheck, at: Path): AppChecks {
|
|
102
|
+
const { yaml, texts } = check;
|
|
103
|
+
return {
|
|
104
|
+
imageExists: (image) => {
|
|
105
|
+
const file = join(check.root, "src", image);
|
|
106
|
+
if (!existsSync(file)) return false;
|
|
107
|
+
check.images.set(image, file);
|
|
108
|
+
return true;
|
|
109
|
+
},
|
|
110
|
+
onLink: (link) =>
|
|
111
|
+
texts.links.push({ yaml, path: [...at, ...link.path], lang: link.lang, offset: 0, href: link.href }),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A block that shows content, checked with the checks the quiz's questions share. */
|
|
116
|
+
function checkMaterialBlock(check: PageCheck, block: MaterialBlock<Text>, at: Path): void {
|
|
117
|
+
const { yaml, problems, texts } = check;
|
|
118
|
+
// the block is the one of the list [block]: its path starts with 0, which `at` already names
|
|
119
|
+
const app = appChecks(check, at);
|
|
120
|
+
const found = checkMaterial([block], {
|
|
121
|
+
languages: texts.languages,
|
|
122
|
+
...app,
|
|
123
|
+
onLink: (link) => app.onLink?.({ ...link, path: link.path.slice(1) }),
|
|
124
|
+
});
|
|
125
|
+
for (const problem of found) {
|
|
126
|
+
problems.at(yaml, [...at, ...problem.path.slice(1)], problem.message, problem.lang, problem.line);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function checkBlocks(check: PageCheck, blocks: readonly Block[], path: Path, depth: number, box?: string): void {
|
|
131
|
+
const ids = new Set<string>();
|
|
132
|
+
blocks.forEach((block, index) => {
|
|
133
|
+
const at = [...path, index];
|
|
134
|
+
const { yaml, texts, problems } = check;
|
|
135
|
+
switch (block.type) {
|
|
136
|
+
case "section": {
|
|
137
|
+
if (box !== undefined) {
|
|
138
|
+
problems.at(yaml, at, `a section inside ${box === "example" ? "an" : "a"} ${box}; a box holds no sections`);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
if (depth >= 2) {
|
|
142
|
+
problems.at(
|
|
143
|
+
yaml,
|
|
144
|
+
at,
|
|
145
|
+
"a section inside a subsection; sections go two levels deep at most, so split the page",
|
|
146
|
+
);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (ids.has(block.id)) {
|
|
150
|
+
problems.at(yaml, [...at, "id"], `${block.id} is used twice at this level; give each section its own id`);
|
|
151
|
+
}
|
|
152
|
+
ids.add(block.id);
|
|
153
|
+
const anchor = [...(depth === 1 ? [String(get(yaml.value, [...path.slice(0, -1), "id"]))] : []), block.id];
|
|
154
|
+
check.anchors.add(anchor.join("/"));
|
|
155
|
+
texts.check(yaml, [...at, "title"], "plain");
|
|
156
|
+
checkBlocks(check, block.blocks, [...at, "blocks"], depth + 1);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case "example":
|
|
160
|
+
case "note":
|
|
161
|
+
if (box !== undefined) {
|
|
162
|
+
problems.at(
|
|
163
|
+
yaml,
|
|
164
|
+
at,
|
|
165
|
+
`${block.type === "example" ? "an" : "a"} ${block.type} inside a ${box}; a box holds no boxes`,
|
|
166
|
+
);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
texts.check(yaml, [...at, "title"], "plain");
|
|
170
|
+
checkBlocks(check, block.blocks, [...at, "blocks"], depth, block.type);
|
|
171
|
+
break;
|
|
172
|
+
case "text":
|
|
173
|
+
case "formula":
|
|
174
|
+
case "figure":
|
|
175
|
+
case "labelled-figure":
|
|
176
|
+
case "table":
|
|
177
|
+
case "quote":
|
|
178
|
+
case "code":
|
|
179
|
+
checkMaterialBlock(check, block, at);
|
|
180
|
+
break;
|
|
181
|
+
case "timeline": {
|
|
182
|
+
if (block.data !== undefined) check.dataUsed.add(block.data);
|
|
183
|
+
if ((block.events === undefined) === (block.data === undefined)) {
|
|
184
|
+
problems.at(
|
|
185
|
+
yaml,
|
|
186
|
+
at,
|
|
187
|
+
block.events === undefined
|
|
188
|
+
? "events or data is missing; a timeline lists its events, or names a data file"
|
|
189
|
+
: "events and data: a timeline lists its events or names a data file, not both",
|
|
190
|
+
);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
if (block.events !== undefined) {
|
|
194
|
+
checkEvents(check, yaml, [...at, "events"], block.events);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
const data = check.dataFiles.get(block.data ?? "");
|
|
198
|
+
if (data === undefined) {
|
|
199
|
+
problems.at(yaml, [...at, "data"], `src/data/${block.data ?? ""}.yaml does not exist`);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
const records = Array.isArray(data.value) ? (data.value as unknown[]) : [];
|
|
203
|
+
const valid = records.every((record, index) => {
|
|
204
|
+
if (validators.event(record)) return true;
|
|
205
|
+
schemaProblems(
|
|
206
|
+
{ ...data, value: data.value },
|
|
207
|
+
(validators.event.errors ?? []).map((error) => ({
|
|
208
|
+
...error,
|
|
209
|
+
instancePath: `/${index}${error.instancePath}`,
|
|
210
|
+
})),
|
|
211
|
+
problems,
|
|
212
|
+
);
|
|
213
|
+
return false;
|
|
214
|
+
});
|
|
215
|
+
if (valid) checkEvents(check, data, [], records as Event[]);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
case "definition": {
|
|
219
|
+
texts.check(yaml, [...at, "term"], "plain");
|
|
220
|
+
texts.check(yaml, [...at, "explanation"], "markdown");
|
|
221
|
+
const id = termId(block.term);
|
|
222
|
+
const other = check.terms.get(id);
|
|
223
|
+
if (other !== undefined) {
|
|
224
|
+
problems.at(
|
|
225
|
+
yaml,
|
|
226
|
+
[...at, "term"],
|
|
227
|
+
`${inLanguage(block.term, "de")} is defined twice (also in ${other}); define each term once`,
|
|
228
|
+
);
|
|
229
|
+
} else check.terms.set(id, yaml.file);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case "generated":
|
|
233
|
+
checkExerciseId(check, block.id, at);
|
|
234
|
+
checkGenerated(check, block, at);
|
|
235
|
+
break;
|
|
236
|
+
case "open-task":
|
|
237
|
+
checkExerciseId(check, block.id, at);
|
|
238
|
+
texts.check(yaml, [...at, "task"], "markdown");
|
|
239
|
+
texts.check(yaml, [...at, "solution"], "markdown");
|
|
240
|
+
if (block.hint !== undefined) texts.check(yaml, [...at, "hint"], "markdown");
|
|
241
|
+
block.points.forEach((point, index) => {
|
|
242
|
+
texts.check(yaml, [...at, "points", index, "text"], "plain");
|
|
243
|
+
// a point's source is a link to a section of the app: checked as every link is (load.ts)
|
|
244
|
+
if (point.source !== undefined)
|
|
245
|
+
texts.links.push({
|
|
246
|
+
yaml,
|
|
247
|
+
path: [...at, "points", index, "source"],
|
|
248
|
+
lang: texts.languages[0] ?? "de",
|
|
249
|
+
offset: 0,
|
|
250
|
+
href: point.source,
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
break;
|
|
254
|
+
case "vue-component":
|
|
255
|
+
checkComponent(check, block, at);
|
|
256
|
+
break;
|
|
257
|
+
case "video":
|
|
258
|
+
// the schema checks its id, title and channel; they are YouTube's, in the video's own language
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
checkExerciseId(check, block.id, at);
|
|
262
|
+
for (const problem of questionDetails(block, texts.languages, undefined, appChecks(check, at))) {
|
|
263
|
+
// a path that ends in the language names it already, as in a block's texts
|
|
264
|
+
const path = [
|
|
265
|
+
...at,
|
|
266
|
+
...problem.path,
|
|
267
|
+
...(problem.lang === undefined || problem.path.at(-1) === problem.lang ? [] : [problem.lang]),
|
|
268
|
+
];
|
|
269
|
+
problems.at(yaml, path, problem.message, problem.lang, problem.line);
|
|
270
|
+
}
|
|
271
|
+
// the texts of a question are plain text with formulas: $…$ must be a formula the build can render
|
|
272
|
+
for (const { path, text } of questionTexts(block)) {
|
|
273
|
+
for (const { lang, text: source, keyed } of textLanguages(text, texts.languages).texts) {
|
|
274
|
+
for (const message of formulaProblems(source)) {
|
|
275
|
+
problems.at(yaml, [...at, ...path, ...(keyed ? [lang] : [])], message, keyed ? lang : undefined);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** A vue-component block: its component exists and passes its checks, its props match, its texts are complete. */
|
|
284
|
+
function checkComponent(check: PageCheck, block: BlockOf<"vue-component">, at: Path): void {
|
|
285
|
+
const { yaml, texts, problems } = check;
|
|
286
|
+
texts.check(yaml, [...at, "nojs"], "markdown");
|
|
287
|
+
for (const name of dataNames(block.props)) check.dataUsed.add(name);
|
|
288
|
+
const component = check.components?.components.get(block.component);
|
|
289
|
+
if (component === undefined) {
|
|
290
|
+
// a component with problems of its own has them reported already
|
|
291
|
+
if (check.components?.names.has(block.component) === true) return;
|
|
292
|
+
const others = [...(check.components?.names ?? [])];
|
|
293
|
+
problems.at(
|
|
294
|
+
yaml,
|
|
295
|
+
[...at, "component"],
|
|
296
|
+
`src/components/${block.component}.vue does not exist; ${others.length === 0 ? "put the component there" : `the components are ${others.join(", ")}`}`,
|
|
297
|
+
);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (block.nojs === undefined && !component.exercise) {
|
|
301
|
+
problems.at(
|
|
302
|
+
yaml,
|
|
303
|
+
at,
|
|
304
|
+
"nojs is missing; a vue-component that is no exercise says in nojs, for readers without JavaScript, what it shows, and gives its content (vue-component-nojs)",
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
// the anchor of an exercise component, which its task's address names (?<id>=<n>): its id, or its name
|
|
308
|
+
let id = block.id;
|
|
309
|
+
if (id === undefined && component.exercise) {
|
|
310
|
+
const base = kebab(component.name);
|
|
311
|
+
id = base;
|
|
312
|
+
for (let n = 2; check.exercises.has(id); n++) id = `${base}-${n}`;
|
|
313
|
+
}
|
|
314
|
+
if (id !== undefined) checkExerciseId(check, id, at);
|
|
315
|
+
// a list prop given as { data: <name> } is the records of that data file from here on
|
|
316
|
+
const files = Object.fromEntries([...check.dataFiles].map(([name, file]) => [name, file.value as object[]]));
|
|
317
|
+
const resolved = withData(block.props, component, files);
|
|
318
|
+
if (resolved.props !== undefined) block.props = resolved.props as NonNullable<typeof block.props>;
|
|
319
|
+
// one line for a problem all records of a data file share
|
|
320
|
+
const found =
|
|
321
|
+
resolved.problems.length > 0
|
|
322
|
+
? resolved.problems
|
|
323
|
+
: propsProblems(block.props, component).filter(
|
|
324
|
+
(problem, index, all) => all.findIndex(({ message }) => message === problem.message) === index,
|
|
325
|
+
);
|
|
326
|
+
for (const problem of found) problems.at(yaml, [...at, "props", ...problem.path], problem.message);
|
|
327
|
+
if (found.length > 0) return;
|
|
328
|
+
for (const [name, field] of Object.entries(component.props)) {
|
|
329
|
+
const value = block.props?.[name];
|
|
330
|
+
for (const path of textPaths(value, field.type)) texts.check(yaml, [...at, "props", name, ...path], "plain");
|
|
331
|
+
}
|
|
332
|
+
// an exercise's check is the component's own code: its examples, at least one answer of each kind, test it
|
|
333
|
+
const examples = block.examples;
|
|
334
|
+
if (!component.exercise) {
|
|
335
|
+
if (examples !== undefined) {
|
|
336
|
+
problems.at(
|
|
337
|
+
yaml,
|
|
338
|
+
[...at, "examples"],
|
|
339
|
+
`${component.name} is no exercise (it emits no answer), so it has no check for examples; remove them`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
const kinds = (["accepted", "rejected"] as const).filter((kind) => (examples?.[kind] ?? []).length === 0);
|
|
344
|
+
if (kinds.length > 0) {
|
|
345
|
+
problems.at(
|
|
346
|
+
yaml,
|
|
347
|
+
examples === undefined ? at : [...at, "examples"],
|
|
348
|
+
`${examples === undefined ? "examples" : kinds.map((kind) => `examples.${kind}`).join(" and ")} ${kinds.length > 1 && examples !== undefined ? "are" : "is"} missing; an exercise component's block lists answers its check must accept (the other correct answers the text allows) and must reject (typical wrong ones), e.g. examples: { accepted: [{ answer: … }], rejected: [{ answer: … }] } (vue-component-exercise-contract)`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const place = (path: Path) => `${yaml.file}:${lineOf(yaml, path, 0) ?? 1}: ${pathText(path)}`;
|
|
353
|
+
check.uses.push({
|
|
354
|
+
block,
|
|
355
|
+
component,
|
|
356
|
+
...(id === undefined ? {} : { id }),
|
|
357
|
+
place: place(at),
|
|
358
|
+
examples: {
|
|
359
|
+
accepted: (examples?.accepted ?? []).map((_example, index) => place([...at, "examples", "accepted", index])),
|
|
360
|
+
rejected: (examples?.rejected ?? []).map((_example, index) => place([...at, "examples", "rejected", index])),
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** A component's name in kebab case, its default anchor: AssignLabels as assign-labels. */
|
|
366
|
+
const kebab = (name: string): string => name.replace(/(?<=[a-z0-9])(?=[A-Z])/g, "-").toLowerCase();
|
|
367
|
+
|
|
368
|
+
/** An exercise's id is an anchor of its page: once on the page. */
|
|
369
|
+
function checkExerciseId(check: PageCheck, id: string, at: Path): void {
|
|
370
|
+
if (RESERVED_PARAMS.includes(id)) {
|
|
371
|
+
check.problems.at(
|
|
372
|
+
check.yaml,
|
|
373
|
+
[...at, "id"],
|
|
374
|
+
`${id} is a parameter of the page's address (${RESERVED_PARAMS.join(", ")}), and the address holds the task number of each exercise under its id; give the exercise another id`,
|
|
375
|
+
);
|
|
376
|
+
} else if (check.exercises.has(id)) {
|
|
377
|
+
check.problems.at(check.yaml, [...at, "id"], `${id} is the id of another exercise on this page; give each its own`);
|
|
378
|
+
} else check.exercises.set(id, at);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const ARBITRARIES = ["int", "decimal", "oneOf", "pick"] as const;
|
|
382
|
+
|
|
383
|
+
/** The parsed expression at `path`, or a problem; the variables it uses must be declared. */
|
|
384
|
+
function checkExpression(check: PageCheck, path: Path, source: string, declared: ReadonlySet<string>): boolean {
|
|
385
|
+
try {
|
|
386
|
+
const used = variables(parse(source));
|
|
387
|
+
const undeclared = [...used].filter((name) => !declared.has(name));
|
|
388
|
+
for (const name of undeclared) {
|
|
389
|
+
check.problems.at(
|
|
390
|
+
check.yaml,
|
|
391
|
+
path,
|
|
392
|
+
`${name} is not declared; declare it in given, or use one of ${[...declared].join(", ")}`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return undeclared.length === 0;
|
|
396
|
+
} catch (error) {
|
|
397
|
+
check.problems.at(
|
|
398
|
+
check.yaml,
|
|
399
|
+
path,
|
|
400
|
+
`the expression "${source}" does not parse: ${error instanceof ExpressionError ? error.message : String(error)}`,
|
|
401
|
+
);
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** A generated exercise: its arbitraries, expressions and texts, then the property test over fixed seeds. */
|
|
407
|
+
function checkGenerated(check: PageCheck, block: Generated, at: Path): void {
|
|
408
|
+
const { yaml, texts, problems } = check;
|
|
409
|
+
const before = problems.lines.length;
|
|
410
|
+
const declared = new Set(Object.keys(block.given));
|
|
411
|
+
const data = (name: string, path: Path) => {
|
|
412
|
+
const file = check.dataFiles.get(name);
|
|
413
|
+
if (file === undefined) problems.at(yaml, path, `src/data/${name}.yaml does not exist`);
|
|
414
|
+
return Array.isArray(file?.value) ? (file.value as unknown[]) : undefined;
|
|
415
|
+
};
|
|
416
|
+
for (const [name, arbitrary] of Object.entries(block.given)) {
|
|
417
|
+
const path = [...at, "given", name];
|
|
418
|
+
for (const used of dataNames(arbitrary.from)) check.dataUsed.add(used);
|
|
419
|
+
const kinds = ARBITRARIES.filter((kind) => arbitrary[kind] !== undefined);
|
|
420
|
+
if (kinds.length !== 1) {
|
|
421
|
+
problems.at(
|
|
422
|
+
yaml,
|
|
423
|
+
path,
|
|
424
|
+
`give exactly one of ${ARBITRARIES.join(", ")}${kinds.length > 1 ? `, not ${kinds.join(" and ")}` : ""}`,
|
|
425
|
+
);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const range = arbitrary.int ?? arbitrary.decimal;
|
|
429
|
+
if (range !== undefined && range[0] > range[1])
|
|
430
|
+
problems.at(yaml, path, `${range[0]} is more than ${range[1]}; write [min, max]`);
|
|
431
|
+
if (arbitrary.decimal !== undefined && arbitrary.step === undefined) {
|
|
432
|
+
problems.at(yaml, path, "a decimal needs its step, e.g. step: 0.1");
|
|
433
|
+
}
|
|
434
|
+
if (arbitrary.step !== undefined && range === undefined) problems.at(yaml, path, "step goes with int or decimal");
|
|
435
|
+
if ((arbitrary.pick === undefined) !== (arbitrary.from === undefined)) {
|
|
436
|
+
problems.at(
|
|
437
|
+
yaml,
|
|
438
|
+
path,
|
|
439
|
+
arbitrary.pick === undefined
|
|
440
|
+
? "from goes with pick: how many to pick"
|
|
441
|
+
: "pick needs from: a list or { data: <name> }",
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
if (arbitrary.distinct !== undefined && arbitrary.pick === undefined)
|
|
445
|
+
problems.at(yaml, path, "distinct goes with pick");
|
|
446
|
+
const source = arbitrary.from;
|
|
447
|
+
let size: number | undefined;
|
|
448
|
+
if (source !== undefined && !Array.isArray(source)) {
|
|
449
|
+
size = data((source as { data: string }).data, [...path, "from", "data"])?.length;
|
|
450
|
+
} else if (Array.isArray(source)) size = source.length;
|
|
451
|
+
if (arbitrary.distinct === true && arbitrary.pick !== undefined && size !== undefined && arbitrary.pick > size) {
|
|
452
|
+
problems.at(yaml, path, `${arbitrary.pick} distinct picks from a list of ${size}; pick fewer`);
|
|
453
|
+
}
|
|
454
|
+
const literals = arbitrary.oneOf ?? (Array.isArray(source) ? source : []);
|
|
455
|
+
const listKey = arbitrary.oneOf !== undefined ? "oneOf" : "from";
|
|
456
|
+
literals.forEach((item, index) => {
|
|
457
|
+
if (isObject(item)) texts.check(yaml, [...path, listKey, index], "plain");
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
(block.where ?? []).forEach((source, index) => checkExpression(check, [...at, "where", index], source, declared));
|
|
461
|
+
checkExpression(check, [...at, "answer", "value"], block.answer.value, declared);
|
|
462
|
+
(block.mistakes ?? []).forEach((mistake, index) => {
|
|
463
|
+
checkExpression(check, [...at, "mistakes", index, "value"], mistake.value, declared);
|
|
464
|
+
texts.check(yaml, [...at, "mistakes", index, "message"], "plain");
|
|
465
|
+
});
|
|
466
|
+
// an example may name the task's right answer too
|
|
467
|
+
const withAnswer = new Set([...declared, "answer"]);
|
|
468
|
+
for (const kind of ["accepted", "rejected"] as const) {
|
|
469
|
+
(block.examples?.[kind] ?? []).forEach((source, index) =>
|
|
470
|
+
checkExpression(check, [...at, "examples", kind, index], source, withAnswer),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
const compare = block.answer.compare;
|
|
474
|
+
if (typeof compare === "object") {
|
|
475
|
+
const options = (compare as Record<string, unknown>)["choice"];
|
|
476
|
+
if (typeof options === "string") checkExpression(check, [...at, "answer", "compare", "choice"], options, declared);
|
|
477
|
+
else if (Array.isArray(options)) {
|
|
478
|
+
options.forEach((item, index) => {
|
|
479
|
+
if (isObject(item)) texts.check(yaml, [...at, "answer", "compare", "choice", index], "plain");
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if ("number" in compare) texts.check(yaml, [...at, "answer", "compare", "number", "unit"], "plain");
|
|
483
|
+
}
|
|
484
|
+
texts.check(yaml, [...at, "task", "text"], "markdown");
|
|
485
|
+
texts.check(yaml, [...at, "hint"], "markdown");
|
|
486
|
+
texts.check(yaml, [...at, "practice"], "plain");
|
|
487
|
+
(block.steps ?? []).forEach((_step, index) => texts.check(yaml, [...at, "steps", index], "markdown"));
|
|
488
|
+
for (const problem of templateProblems(block)) {
|
|
489
|
+
problems.at(yaml, [...at, ...problem.path], problem.message, problem.lang);
|
|
490
|
+
}
|
|
491
|
+
for (const name of unusedVariables(block)) {
|
|
492
|
+
problems.at(
|
|
493
|
+
yaml,
|
|
494
|
+
[...at, "given", name],
|
|
495
|
+
`${name} is never used: no placeholder {{${name}}} in the task, figure, steps, hint or mistakes, and no expression of the figure, answer or mistakes names it; use it, or remove it from given`,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
if (problems.lines.length > before || check.seeds === 0) return;
|
|
499
|
+
// the property test over fixed seeds
|
|
500
|
+
const records: Record<string, DataRecord[]> = {};
|
|
501
|
+
for (const [name, file] of check.dataFiles) records[name] = file.value as DataRecord[];
|
|
502
|
+
let templates: Templates;
|
|
503
|
+
try {
|
|
504
|
+
templates = templatesOf(block, texts.languages);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
const reason = (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
|
|
507
|
+
problems.at(
|
|
508
|
+
yaml,
|
|
509
|
+
at,
|
|
510
|
+
`a formula with its placeholders filled in does not parse (${reason.trim()}); check its braces: a placeholder is {{G}}, a letter is G`,
|
|
511
|
+
);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const sound = soundness(block, dataOf(block, records), templates, texts.languages, check.seeds);
|
|
515
|
+
for (const problem of sound) problems.at(yaml, [...at, ...problem.path], problem.message, problem.lang);
|
|
516
|
+
if (sound.length > 0) return;
|
|
517
|
+
for (const problem of exampleProblems(block, dataOf(block, records), templates, texts.languages, check.seeds)) {
|
|
518
|
+
problems.at(yaml, [...at, ...problem.path], problem.message);
|
|
519
|
+
}
|
|
520
|
+
}
|