@lernapps/app-templates 0.2.0 → 0.3.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/README.md +5 -11
- package/dist/{assessment-Dy359O1O.mjs → assessment-DaKsXUdj.mjs} +204 -16
- package/dist/{blocks-CxtCPAa5.mjs → blocks-D4SSGKuL.mjs} +2 -2
- package/dist/blocks.mjs +2 -2
- package/dist/{check-CteIFN96.mjs → check-BxZOZPic.mjs} +58 -4
- package/dist/{content-DO6oEJxj.mjs → content-CAL0Nz1a.mjs} +1 -1
- package/dist/explainer-e2e.mjs +5 -5
- package/dist/explainer-plugin.mjs +57 -13
- package/dist/explainer.mjs +2 -2
- package/dist/interactive-e2e.mjs +1 -1
- package/dist/interactive-plugin.mjs +2 -2
- package/dist/{load-RosvaPZf.mjs → load--ovXqbNE.mjs} +286 -33
- package/dist/questions.mjs +1 -1
- package/dist/quiz-e2e.mjs +1 -1
- package/dist/quiz-plugin.mjs +3 -3
- package/dist/{render-D5kwkNt1.mjs → render-D3LsJY04.mjs} +2 -2
- package/dist/{render-DqHOJyHK.mjs → render-mYtzN1DV.mjs} +1 -1
- package/dist/{result-D_mmCVwN.mjs → result-DphIWHDF.mjs} +1 -1
- package/dist/{validate-Bxy9NfB8.mjs → validate-CfFykN0M.mjs} +1 -1
- package/dist/{validate-BDwL7Su3.mjs → validate-j3GjVhNF.mjs} +2 -2
- package/explainer/README.md +7 -3
- package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +26 -8
- package/explainer/docs/arc42/09-explainer-decisions.arc42.md +42 -0
- package/explainer/package.json +1 -1
- package/explainer/src/app.yaml +3 -1
- package/explainer/src/prozent/chapter.yaml +1 -1
- package/explainer/src/prozent/grundwert.yaml +2 -1
- package/explainer/src/prozent/prozentsatz.yaml +2 -1
- package/explainer/src/prozent/prozentwert.yaml +2 -1
- package/interactive/package.json +1 -1
- package/package.json +3 -4
- package/quiz/package.json +1 -1
- package/runtime/explainer/assessment.ts +4 -2
- package/runtime/explainer/check-page.ts +6 -2
- package/runtime/explainer/content.ts +20 -1
- package/runtime/explainer/exercises.ts +71 -6
- package/runtime/explainer/explainer.css +21 -0
- package/runtime/explainer/explainer.v1.schema.json +2 -2
- package/runtime/explainer/explainer.v2.schema.json +152 -0
- package/runtime/explainer/expression.ts +127 -1
- package/runtime/explainer/generated/texts.ts +98 -9
- package/runtime/explainer/generated/types.ts +8 -0
- package/runtime/explainer/load.ts +70 -16
- package/runtime/explainer/messages/de.json +5 -1
- package/runtime/explainer/messages/en.json +5 -1
- package/runtime/explainer/outline.ts +1 -1
- package/runtime/explainer/page-list.ts +38 -0
- package/runtime/explainer/plugin.ts +13 -4
- package/runtime/explainer/render.ts +26 -2
- package/runtime/explainer/schema.ts +92 -9
- package/runtime/explainer/tutorpage.ts +9 -1
- package/runtime/shared/blocks/blocks.v1.schema.json +1 -1
- package/runtime/shared/text/formula.ts +61 -1
- package/scaffold/explainer/src/app.yaml +5 -1
- package/scaffold/explainer/src/thema/chapter.yaml +1 -1
- package/scaffold/explainer/src/thema/einstieg.yaml +1 -1
- package/scaffold/interactive/src/core/index.ts +4 -2
- package/scaffold/interactive/src/data/count.json +1 -0
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
// depth. The bundled images go to images/, the tutor page of each content page to <chapter>/<page>/tutor/ with its
|
|
13
13
|
// Markdown beside it (index.md; tutorpage.ts), and of each self-assessment to <assessment>/tutor/ (assessment-page.ts),
|
|
14
14
|
// every link of them checked, and 404.html, which leads every address the
|
|
15
|
-
// app does not have to its start page (notFoundPage of ../shared/pages.ts).
|
|
15
|
+
// app does not have to its start page (notFoundPage of ../shared/pages.ts). An app of format 2 also gets
|
|
16
|
+
// lernapps-pages.json at the root, the list of its pages with their competencies for the catalog (page-list.ts). `vp dev` serves the same pages and tutor pages, rendered on each request.
|
|
16
17
|
//
|
|
17
18
|
// Components (src/components/<Name>.vue, components.ts): only when the app has them, the plugin compiles them with
|
|
18
19
|
// @vitejs/plugin-vue and resolves vue to the copy this runtime depends on, so that the components and the islands
|
|
@@ -53,6 +54,7 @@ import {
|
|
|
53
54
|
tutorPageDocument,
|
|
54
55
|
} from "./tutorpage.ts";
|
|
55
56
|
import { imageType } from "../shared/blocks/image.ts";
|
|
57
|
+
import { PAGE_LIST, pageList } from "./page-list.ts";
|
|
56
58
|
|
|
57
59
|
const MARKER = "<!-- explainer -->";
|
|
58
60
|
const ISLANDS = "\0lernapps-explainer-islands";
|
|
@@ -263,7 +265,9 @@ export function explainer(): Plugin[] {
|
|
|
263
265
|
}
|
|
264
266
|
if (command !== "build" || content.components.length === 0) return;
|
|
265
267
|
const rendered = await withServer(root, (loader) => renderComponents(content, loader, true));
|
|
266
|
-
if (rendered.problems.length > 0)
|
|
268
|
+
if (rendered.problems.length > 0) {
|
|
269
|
+
this.error(new ContentError(rendered.problems, content.app.format ?? 1).message);
|
|
270
|
+
}
|
|
267
271
|
islands = rendered.islands;
|
|
268
272
|
descriptions = rendered.descriptions;
|
|
269
273
|
islandsChunk = this.emitFile({ type: "chunk", id: ISLANDS, name: "islands" });
|
|
@@ -289,6 +293,11 @@ export function explainer(): Plugin[] {
|
|
|
289
293
|
else this.emitFile({ type: "asset", fileName: `${page.route.path}index.html`, source: html });
|
|
290
294
|
}
|
|
291
295
|
this.emitFile({ type: "asset", fileName: "404.html", source: notFoundPage() });
|
|
296
|
+
// format 2: the list of the pages for the catalog
|
|
297
|
+
const list = pageList(content);
|
|
298
|
+
if (list !== undefined) {
|
|
299
|
+
this.emitFile({ type: "asset", fileName: PAGE_LIST, source: `${JSON.stringify(list, null, 2)}\n` });
|
|
300
|
+
}
|
|
292
301
|
for (const [image, file] of content.images) {
|
|
293
302
|
this.emitFile({ type: "asset", fileName: image, source: readFileSync(file) });
|
|
294
303
|
}
|
|
@@ -404,7 +413,7 @@ export function explainer(): Plugin[] {
|
|
|
404
413
|
);
|
|
405
414
|
}
|
|
406
415
|
}
|
|
407
|
-
if (problems.length > 0) this.error(new ContentError(problems).message);
|
|
416
|
+
if (problems.length > 0) this.error(new ContentError(problems, content.app.format ?? 1).message);
|
|
408
417
|
},
|
|
409
418
|
configureServer(server) {
|
|
410
419
|
devServer = server;
|
|
@@ -442,7 +451,7 @@ export function explainer(): Plugin[] {
|
|
|
442
451
|
if (dev === undefined) return next();
|
|
443
452
|
renderComponents(content, (id) => dev.ssrLoadModule(id), false)
|
|
444
453
|
.then(({ islands: rendered, descriptions: described, problems }) => {
|
|
445
|
-
if (problems.length > 0) throw new ContentError(problems);
|
|
454
|
+
if (problems.length > 0) throw new ContentError(problems, content.app.format ?? 1);
|
|
446
455
|
if (forScope) {
|
|
447
456
|
const markdown = path.endsWith(TUTOR_MARKDOWN);
|
|
448
457
|
const lang =
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// as inline SVG or an image, tables, timelines, definitions, quotes, highlighted code, the exercises (exercises.ts:
|
|
3
3
|
// each with its solution in a <details>, a generated one with three worked tasks), and the navigation (contents,
|
|
4
4
|
// breadcrumbs, previous and next, the contents of a page with three or more sections, the glossary, the language
|
|
5
|
-
// switch
|
|
5
|
+
// switch; a recap page marked in the contents, and at its top its notice with the link to the learning map). Without
|
|
6
|
+
// JavaScript the page is complete; the client module only adds. A vue-component block is its island:
|
|
6
7
|
// the component's markup as the build rendered it (`hidden` until it hydrates), its nojs text, its props in the page's
|
|
7
8
|
// language, and for an exercise its anchor, its task number with "Neue Aufgabe", the task of number 1 as its describe
|
|
8
9
|
// states it with the solution folded, and the place of its feedback. Links to other sites are plain links with rel="noopener":
|
|
@@ -13,6 +14,7 @@ import { questionMessagesIn } from "../shared/questions/render.ts";
|
|
|
13
14
|
import { propsIn } from "./components/props.ts";
|
|
14
15
|
import {
|
|
15
16
|
inLanguage,
|
|
17
|
+
mapUrl,
|
|
16
18
|
termId as termIdOf,
|
|
17
19
|
timelineEvents,
|
|
18
20
|
type Block,
|
|
@@ -322,7 +324,10 @@ function pageList(context: Context, pages: readonly NonNullable<Route["page"]>[]
|
|
|
322
324
|
page.description === undefined
|
|
323
325
|
? ""
|
|
324
326
|
: ` <span class="explainer-description">${context.text(page.description)}</span>`;
|
|
325
|
-
|
|
327
|
+
// a recap page repeats a competency the app does not practise: marked, so that no one takes it for the app's own
|
|
328
|
+
const recap =
|
|
329
|
+
page.recap === undefined ? "" : ` <span class="explainer-recap-note">(${plain(context.t("recapNote"))})</span>`;
|
|
330
|
+
return `<li><a href="${context.to(`${page.chapter}/${page.id}/`)}">${context.text(page.title)}</a>${recap}${description}</li>`;
|
|
326
331
|
})
|
|
327
332
|
.join("")}</ol>`;
|
|
328
333
|
}
|
|
@@ -407,6 +412,7 @@ function main(context: Context): string {
|
|
|
407
412
|
title,
|
|
408
413
|
tutorSection(context),
|
|
409
414
|
`</div>`,
|
|
415
|
+
page.recap === undefined ? "" : recapNotice(context, page.recap),
|
|
410
416
|
pageContents(context, page.blocks),
|
|
411
417
|
renderBlocks(context, page.blocks, 1),
|
|
412
418
|
].join("\n");
|
|
@@ -414,6 +420,24 @@ function main(context: Context): string {
|
|
|
414
420
|
}
|
|
415
421
|
}
|
|
416
422
|
|
|
423
|
+
/**
|
|
424
|
+
* At the top of a recap page: that it is a recap, and the link to the competency's page on the learning map, where the
|
|
425
|
+
* apps are that practise it. The runtime writes it, so that it cannot be missing; a plain link, nothing loaded before
|
|
426
|
+
* a click.
|
|
427
|
+
*/
|
|
428
|
+
function recapNotice(context: Context, recap: string): string {
|
|
429
|
+
const link = external(mapUrl(recap), context.t("recapLink"));
|
|
430
|
+
// the message keeps its placeholder {link} unfilled: the link goes in its place as markup
|
|
431
|
+
const [before = "", after = ""] = context.t("recapText").split("{link}");
|
|
432
|
+
const label = plain(context.t("recap"));
|
|
433
|
+
return [
|
|
434
|
+
`<aside class="explainer-recap" aria-label="${label}">`,
|
|
435
|
+
`<p class="explainer-recap-label">${label}</p>`,
|
|
436
|
+
`<p>${plain(before)}${link}${plain(after)}</p>`,
|
|
437
|
+
`</aside>`,
|
|
438
|
+
].join("\n");
|
|
439
|
+
}
|
|
440
|
+
|
|
417
441
|
/** What a part of a self-assessment needs of the page it is on (assessment-page.ts). */
|
|
418
442
|
const assessmentContext = (context: Context): AssessmentContext => ({
|
|
419
443
|
site: context.site,
|
|
@@ -1,25 +1,35 @@
|
|
|
1
|
-
// The explainer's content
|
|
2
|
-
//
|
|
1
|
+
// The explainer's content schemas at build time: format 1 (explainer.v1.schema.json) and format 2
|
|
2
|
+
// (explainer.v2.schema.json, which adds the strand of the app, the competencies of each page and recap pages, and refers to format 1
|
|
3
|
+
// for everything else); the validators of their files (the app, a chapter, a page, a data file) by format, and the explainer's own words for the errors of the schema (what a quote or a bundled
|
|
3
4
|
// image lacks, which fields a block type has, which an exercise of a question type); the general messages are
|
|
4
5
|
// shared/content.ts's.
|
|
5
6
|
import { Ajv2020, type ValidateFunction } from "ajv/dist/2020.js";
|
|
6
7
|
import schema from "./explainer.v1.schema.json" with { type: "json" };
|
|
8
|
+
import schemaV2 from "./explainer.v2.schema.json" with { type: "json" };
|
|
7
9
|
import questionSchema from "../shared/questions/question.v1.schema.json" with { type: "json" };
|
|
8
10
|
import { knownProperties, schemasById } from "../shared/schema.ts";
|
|
9
11
|
import { addressProblem, QUESTION_BRANCH, questionSchemas } from "../shared/questions/validate.ts";
|
|
10
12
|
import { isObject, schemaProblems, type DescribeError, type Problems, type YamlFile } from "../shared/content.ts";
|
|
11
13
|
|
|
12
14
|
export const SCHEMA_ID = schema.$id;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
export const SCHEMA_ID_V2 = schemaV2.$id;
|
|
16
|
+
/** The formats of the content: 1 without `format` in src/app.yaml, 2 with `format: 2`. */
|
|
17
|
+
export type Format = 1 | 2;
|
|
18
|
+
/** Where an app finds the schema of its content, by format, for the message that names it. */
|
|
19
|
+
export const SCHEMA_FILES: Readonly<Record<Format, string>> = {
|
|
20
|
+
1: "node_modules/@lernapps/app-templates/runtime/explainer/explainer.v1.schema.json",
|
|
21
|
+
2: "node_modules/@lernapps/app-templates/runtime/explainer/explainer.v2.schema.json",
|
|
22
|
+
};
|
|
23
|
+
export const SCHEMA_FILE = SCHEMA_FILES[1];
|
|
15
24
|
|
|
16
25
|
// union types ("type": ["string", "object"]) keep the messages of the comparator and the lists plain
|
|
17
26
|
const ajv = new Ajv2020({ allErrors: true, strict: true, allowUnionTypes: true });
|
|
18
27
|
for (const each of questionSchemas) ajv.addSchema(each);
|
|
19
28
|
ajv.addSchema(schema);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
ajv.addSchema(schemaV2);
|
|
30
|
+
const validator = (name: string, id = SCHEMA_ID): ValidateFunction => {
|
|
31
|
+
const validate = ajv.getSchema(`${id}#/$defs/${name}`);
|
|
32
|
+
if (validate === undefined) throw new Error(`the schema ${id} has no $defs/${name}`);
|
|
23
33
|
return validate;
|
|
24
34
|
};
|
|
25
35
|
export const validators = {
|
|
@@ -29,6 +39,20 @@ export const validators = {
|
|
|
29
39
|
data: validator("data"),
|
|
30
40
|
event: validator("event"),
|
|
31
41
|
};
|
|
42
|
+
/** The validators of format 2: its own app and page; chapters, data files and events are those of format 1. */
|
|
43
|
+
const validatorsV2: typeof validators = {
|
|
44
|
+
...validators,
|
|
45
|
+
app: validator("app", SCHEMA_ID_V2),
|
|
46
|
+
page: validator("page", SCHEMA_ID_V2),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** The format of the app `app` (src/app.yaml as read): 2 when it names any format, which its schema then checks. */
|
|
50
|
+
export const formatOf = (app: unknown): Format => (isObject(app) && app["format"] !== undefined ? 2 : 1);
|
|
51
|
+
|
|
52
|
+
/** The fields of format 2, which format 1 does not know. */
|
|
53
|
+
const FORMAT_2_FIELDS = new Set(["format", "strand", "competencies", "recap"]);
|
|
54
|
+
/** The fields of format 2 on a page. */
|
|
55
|
+
const PAGE_FIELDS = new Set(["competencies", "recap"]);
|
|
32
56
|
|
|
33
57
|
const schemas = schemasById(...questionSchemas, schema);
|
|
34
58
|
|
|
@@ -61,6 +85,9 @@ const questionFields = (type: string): Set<string> => {
|
|
|
61
85
|
/** What `required` names as missing, with why it is needed. */
|
|
62
86
|
function missing(property: string, object: unknown): string {
|
|
63
87
|
const type = isObject(object) ? object["type"] : undefined;
|
|
88
|
+
if (property === "strand") {
|
|
89
|
+
return "strand is missing; name the strand of the learning map the app stays in, <subject>/<strand> (e.g. mathematik/stochastik), or none when the app is on no strand";
|
|
90
|
+
}
|
|
64
91
|
if (property === "origin") return "origin is missing; a quote names who said or wrote it, where and when";
|
|
65
92
|
if ((property === "licence" || property === "source") && type === "quote") {
|
|
66
93
|
return `${property} is missing; a quote names its source and its licence`;
|
|
@@ -80,7 +107,58 @@ const describe: DescribeError = (error, path, value) => {
|
|
|
80
107
|
const { lang, message } = addressProblem(value);
|
|
81
108
|
return { message, ...(lang === undefined ? {} : { lang }) };
|
|
82
109
|
}
|
|
110
|
+
case "not":
|
|
111
|
+
// a page with both: it practises competencies of the app's strand, or it repeats one the app does not practise
|
|
112
|
+
if (path.length !== 0 || !isObject(value) || !("recap" in value) || !("competencies" in value)) return undefined;
|
|
113
|
+
return {
|
|
114
|
+
path: ["recap"],
|
|
115
|
+
message:
|
|
116
|
+
"a page has competencies or recap, never both: a page practises competencies of the app's strand, a recap page only repeats a competency the app does not practise; remove one of them",
|
|
117
|
+
};
|
|
118
|
+
case "const":
|
|
119
|
+
// the format of the content: 2, or none at all for format 1
|
|
120
|
+
if (path.length !== 1 || path[0] !== "format") return undefined;
|
|
121
|
+
return { message: `${String(value)} is not a format; write format: 2, or leave format out for format 1` };
|
|
122
|
+
case "uniqueItems":
|
|
123
|
+
if (path.at(-1) !== "competencies") return undefined;
|
|
124
|
+
return { message: "names a competency twice; name each competency once" };
|
|
125
|
+
case "additionalProperties": {
|
|
126
|
+
// a field of format 2 in an app of format 1
|
|
127
|
+
const property = String(error.params["additionalProperty"]);
|
|
128
|
+
if (path.length !== 0 || !FORMAT_2_FIELDS.has(property) || !isObject(value)) return undefined;
|
|
129
|
+
if (PAGE_FIELDS.has(property) && "blocks" in value) {
|
|
130
|
+
return {
|
|
131
|
+
path: [property],
|
|
132
|
+
message: `${property} is a field of format 2; add format: 2 and the app's strand to src/app.yaml, or remove it`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (!PAGE_FIELDS.has(property) && "chapters" in value) {
|
|
136
|
+
return {
|
|
137
|
+
path: [property],
|
|
138
|
+
message: `${property} is a field of format 2; add format: 2 to src/app.yaml, with the app's strand`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
83
143
|
case "pattern":
|
|
144
|
+
// the strand of the app: <subject>/<strand> or none
|
|
145
|
+
if (path.length === 1 && path[0] === "strand") {
|
|
146
|
+
return {
|
|
147
|
+
message: `${String(value)} is no strand; write <subject>/<strand> with the ids of the learning map, e.g. mathematik/stochastik, or none`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// the competency a recap page repeats: <subject>/<competency>
|
|
151
|
+
if (path.length === 1 && path[0] === "recap") {
|
|
152
|
+
return {
|
|
153
|
+
message: `${String(value)} is no competency of the learning map; write <subject>/<competency> with the ids of the learning map, e.g. physik/gewichtskraft-berechnen`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// a competency of a page: an id of the learning map
|
|
157
|
+
if (path.at(-2) === "competencies") {
|
|
158
|
+
return {
|
|
159
|
+
message: `${String(value)} is no competency id; use the competency's id in the learning map: lower-case letters, digits and hyphens, e.g. laplace`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
84
162
|
// an exercise of a self-assessment: by its page and id
|
|
85
163
|
if (path.at(-2) === "exercises" && path[0] === "selfAssessment") {
|
|
86
164
|
return { message: `${String(value)} is no exercise's address; write /<chapter>/<page>/#<exercise>` };
|
|
@@ -129,8 +207,13 @@ const describe: DescribeError = (error, path, value) => {
|
|
|
129
207
|
return undefined;
|
|
130
208
|
};
|
|
131
209
|
|
|
132
|
-
export function validate(
|
|
133
|
-
|
|
210
|
+
export function validate(
|
|
211
|
+
yaml: YamlFile,
|
|
212
|
+
kind: keyof typeof validators,
|
|
213
|
+
problems: Problems,
|
|
214
|
+
format: Format = 1,
|
|
215
|
+
): boolean {
|
|
216
|
+
const check = (format === 2 ? validatorsV2 : validators)[kind];
|
|
134
217
|
if (check(yaml.value)) return true;
|
|
135
218
|
const before = problems.lines.length;
|
|
136
219
|
schemaProblems(yaml, check.errors ?? [], problems, describe);
|
|
@@ -27,6 +27,7 @@ import { escape } from "../shared/html.ts";
|
|
|
27
27
|
import { answerText } from "../shared/questions/render.ts";
|
|
28
28
|
import {
|
|
29
29
|
inLanguage,
|
|
30
|
+
mapUrl,
|
|
30
31
|
type Block,
|
|
31
32
|
type ComponentExercise,
|
|
32
33
|
type Descriptions,
|
|
@@ -72,6 +73,8 @@ interface Words {
|
|
|
72
73
|
needsIntro: string;
|
|
73
74
|
needs: Readonly<Record<Need, string>>;
|
|
74
75
|
contextTitle: string;
|
|
76
|
+
/** On a recap page: that it has no exercise, and where the competency is practised ({url}). */
|
|
77
|
+
recap: string;
|
|
75
78
|
exercise: string;
|
|
76
79
|
steps: string;
|
|
77
80
|
link: string;
|
|
@@ -139,6 +142,8 @@ const WORDS: Readonly<Record<string, Words>> = {
|
|
|
139
142
|
"Zeig Zahlen der Seite als Menge, z. B. „37 sind 3 Zehner und 7 Einer“, aber nie das Ergebnis einer Aufgabe. Ein Schritt pro Nachricht, nie Zeitdruck.",
|
|
140
143
|
},
|
|
141
144
|
contextTitle: "Kontext: die Seite „{page}“",
|
|
145
|
+
recap:
|
|
146
|
+
"Diese Seite ist eine Wiederholung: Sie wiederholt etwas, das das Kind schon können sollte, und hat keine Übung. Erklär mit dem Inhalt der Seite und frag nach, ob das Kind ihn verstanden hat. Üben kann es das in anderen Apps; welche das sind, steht hier: {url}",
|
|
142
147
|
exercise: "Übung",
|
|
143
148
|
steps: "Lösungsweg",
|
|
144
149
|
link: "Link",
|
|
@@ -205,6 +210,8 @@ const WORDS: Readonly<Record<string, Words>> = {
|
|
|
205
210
|
"Show numbers of the page as amounts, e.g. “37 is 3 tens and 7 ones”, but never the result of a task. One step per message, never time pressure.",
|
|
206
211
|
},
|
|
207
212
|
contextTitle: "Context: the page “{page}”",
|
|
213
|
+
recap:
|
|
214
|
+
"This page is a recap: it goes over something the child should already know, and has no exercise. Explain with the content of the page and ask whether the child has understood it. It can practise it in other apps; which ones, it says here: {url}",
|
|
208
215
|
exercise: "Exercise",
|
|
209
216
|
steps: "Steps of the solution",
|
|
210
217
|
link: "Link",
|
|
@@ -341,7 +348,7 @@ export function generatedLines(spec: Generated, site: Site, lang: string, format
|
|
|
341
348
|
lines.push(`- ${words.linkNumber}: \`${page}?${spec.id}=n&via=tutor#${spec.id}\``);
|
|
342
349
|
const { paths, answer } = placeholdersOf(spec, lang);
|
|
343
350
|
const data = dataOf(spec, site.data);
|
|
344
|
-
const templates = templatesOf(spec, [lang]);
|
|
351
|
+
const templates = templatesOf(spec, [lang], data);
|
|
345
352
|
let choices = false;
|
|
346
353
|
const rows = taskSeeds(spec, data).map((seed, index) => {
|
|
347
354
|
const task = generate(spec, seed, data);
|
|
@@ -569,6 +576,7 @@ function pageMarkdown(
|
|
|
569
576
|
`## ${fill(words.contextTitle)}`,
|
|
570
577
|
"",
|
|
571
578
|
);
|
|
579
|
+
if (page.recap !== undefined) lines.push(words.recap.replace("{url}", mapUrl(page.recap)), "");
|
|
572
580
|
if (page.description !== undefined) lines.push(own(page.description), "");
|
|
573
581
|
lines.push(
|
|
574
582
|
...contentLines(
|
|
@@ -328,7 +328,7 @@
|
|
|
328
328
|
"type": "object"
|
|
329
329
|
},
|
|
330
330
|
"latex": {
|
|
331
|
-
"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.",
|
|
331
|
+
"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), \\ (space), \\quad, \\qquad, \\Rightarrow, \\Leftrightarrow, \\left( … \\right), parentheses and braces; \\sin, \\cos, \\tan, \\arcsin, \\arccos, \\arctan (or \\sin^{-1}), \\circ for degrees (30^\\circ); the Greek letters \\alpha to \\omega (also \\varepsilon, \\vartheta, \\varphi) and the capitals \\Gamma, \\Delta, \\Theta, \\Lambda, \\Xi, \\Pi, \\Sigma, \\Phi, \\Psi, \\Omega; \\ce{…} for chemical equations and \\pu{…} for units (mhchem). The build names any other command and lists these. Written without the $ signs. A map by language only when it has \\text{…} that differs by language.",
|
|
332
332
|
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
333
333
|
},
|
|
334
334
|
"table": {
|
|
@@ -7,6 +7,46 @@ import temml from "temml";
|
|
|
7
7
|
const mhchem: string = "temml/contrib/mhchem/mhchem.js";
|
|
8
8
|
await import(mhchem);
|
|
9
9
|
|
|
10
|
+
/** The Greek letters a formula may use, as \\alpha: the small ones, and the capitals that differ from Latin ones. */
|
|
11
|
+
const GREEK = [
|
|
12
|
+
"alpha",
|
|
13
|
+
"beta",
|
|
14
|
+
"gamma",
|
|
15
|
+
"delta",
|
|
16
|
+
"varepsilon",
|
|
17
|
+
"epsilon",
|
|
18
|
+
"zeta",
|
|
19
|
+
"eta",
|
|
20
|
+
"theta",
|
|
21
|
+
"vartheta",
|
|
22
|
+
"iota",
|
|
23
|
+
"kappa",
|
|
24
|
+
"lambda",
|
|
25
|
+
"mu",
|
|
26
|
+
"nu",
|
|
27
|
+
"xi",
|
|
28
|
+
"pi",
|
|
29
|
+
"rho",
|
|
30
|
+
"sigma",
|
|
31
|
+
"tau",
|
|
32
|
+
"upsilon",
|
|
33
|
+
"varphi",
|
|
34
|
+
"phi",
|
|
35
|
+
"chi",
|
|
36
|
+
"psi",
|
|
37
|
+
"omega",
|
|
38
|
+
"Gamma",
|
|
39
|
+
"Delta",
|
|
40
|
+
"Theta",
|
|
41
|
+
"Lambda",
|
|
42
|
+
"Xi",
|
|
43
|
+
"Pi",
|
|
44
|
+
"Sigma",
|
|
45
|
+
"Phi",
|
|
46
|
+
"Psi",
|
|
47
|
+
"Omega",
|
|
48
|
+
];
|
|
49
|
+
|
|
10
50
|
/** The commands a formula may use (explainer.v1.schema.json, $defs/latex), besides ^, _, braces and parentheses. */
|
|
11
51
|
export const COMMANDS: readonly string[] = [
|
|
12
52
|
"\\frac",
|
|
@@ -26,6 +66,25 @@ export const COMMANDS: readonly string[] = [
|
|
|
26
66
|
"\\}",
|
|
27
67
|
"\\ce",
|
|
28
68
|
"\\pu",
|
|
69
|
+
// spaces and arrows between the steps of a calculation
|
|
70
|
+
"\\ ",
|
|
71
|
+
"\\quad",
|
|
72
|
+
"\\qquad",
|
|
73
|
+
"\\Rightarrow",
|
|
74
|
+
"\\Leftrightarrow",
|
|
75
|
+
// parentheses that grow with their content
|
|
76
|
+
"\\left",
|
|
77
|
+
"\\right",
|
|
78
|
+
// trigonometry: the functions, their inverses (or \sin^{-1}) and the degree sign (^\circ)
|
|
79
|
+
"\\sin",
|
|
80
|
+
"\\cos",
|
|
81
|
+
"\\tan",
|
|
82
|
+
"\\arcsin",
|
|
83
|
+
"\\arccos",
|
|
84
|
+
"\\arctan",
|
|
85
|
+
"\\circ",
|
|
86
|
+
// Greek letters
|
|
87
|
+
...GREEK.map((letter) => `\\${letter}`),
|
|
29
88
|
];
|
|
30
89
|
const allowed = new Set(COMMANDS);
|
|
31
90
|
|
|
@@ -52,7 +111,8 @@ function commands(tex: string): string[] {
|
|
|
52
111
|
export function formulaProblem(tex: string): string | undefined {
|
|
53
112
|
const unknown = commands(tex).find((command) => !allowed.has(command));
|
|
54
113
|
if (unknown !== undefined) {
|
|
55
|
-
|
|
114
|
+
const list = COMMANDS.map((command) => (command === "\\ " ? "\\ (a space)" : command)).join(", ");
|
|
115
|
+
return `${unknown} is not in the formula command list; use only these commands, besides ^, _, braces and parentheses: ${list}`;
|
|
56
116
|
}
|
|
57
117
|
try {
|
|
58
118
|
temml.renderToString(tex, { throwOnError: true });
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
# yaml-language-server: $schema=../node_modules/@lernapps/app-templates/runtime/explainer/explainer.
|
|
1
|
+
# yaml-language-server: $schema=../node_modules/@lernapps/app-templates/runtime/explainer/explainer.v2.schema.json
|
|
2
|
+
format: 2
|
|
3
|
+
# The strand of the learning map the app stays in, <subject>/<strand> (e.g. mathematik/stochastik); then each page
|
|
4
|
+
# names its competencies. none: the app is on no strand, and no page names competencies.
|
|
5
|
+
strand: none
|
|
2
6
|
title: Platzhalter
|
|
3
7
|
intro: Dieses Kapitel hält den Platz frei, bis der geplante Inhalt da ist.
|
|
4
8
|
languages: [de]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# yaml-language-server: $schema=../../node_modules/@lernapps/app-templates/runtime/explainer/explainer.
|
|
1
|
+
# yaml-language-server: $schema=../../node_modules/@lernapps/app-templates/runtime/explainer/explainer.v2.schema.json
|
|
2
2
|
title: Thema
|
|
3
3
|
summary: Ersetze dieses Kapitel durch das erste geplante.
|
|
4
4
|
pages: [einstieg]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# yaml-language-server: $schema=../../node_modules/@lernapps/app-templates/runtime/explainer/explainer.
|
|
1
|
+
# yaml-language-server: $schema=../../node_modules/@lernapps/app-templates/runtime/explainer/explainer.v2.schema.json
|
|
2
2
|
title: Einstieg
|
|
3
3
|
description: Eine Seite, die den Platz frei hält.
|
|
4
4
|
blocks:
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// The app's logic, a placeholder: a count from 0 to
|
|
1
|
+
// The app's logic, a placeholder: a count from 0 to the goal of src/data/count.json, one step for each "weiter" (the
|
|
2
|
+
// button of the component).
|
|
2
3
|
// Pure TypeScript without DOM; the runtime keeps the state and calls start and step
|
|
3
4
|
// (@lernapps/app-templates/interactive). Replace it with the planned core.
|
|
4
5
|
import type { Input } from "@lernapps/app-templates/interactive";
|
|
6
|
+
import count from "../data/count.json" with { type: "json" };
|
|
5
7
|
|
|
6
8
|
export interface State {
|
|
7
9
|
seed: number;
|
|
@@ -10,7 +12,7 @@ export interface State {
|
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
/** How far the count goes. */
|
|
13
|
-
export const GOAL =
|
|
15
|
+
export const GOAL: number = count.goal;
|
|
14
16
|
|
|
15
17
|
export function start(seed: number): State {
|
|
16
18
|
return { seed, count: 0, done: false };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "goal": 3 }
|