@lernapps/app-templates 0.2.1 → 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.
Files changed (56) hide show
  1. package/README.md +5 -11
  2. package/dist/{assessment-Dy359O1O.mjs → assessment-DaKsXUdj.mjs} +204 -16
  3. package/dist/{blocks-CxtCPAa5.mjs → blocks-D4SSGKuL.mjs} +2 -2
  4. package/dist/blocks.mjs +2 -2
  5. package/dist/{check-CteIFN96.mjs → check-BxZOZPic.mjs} +58 -4
  6. package/dist/{content-DO6oEJxj.mjs → content-CAL0Nz1a.mjs} +1 -1
  7. package/dist/explainer-e2e.mjs +5 -5
  8. package/dist/explainer-plugin.mjs +57 -13
  9. package/dist/explainer.mjs +2 -2
  10. package/dist/interactive-e2e.mjs +1 -1
  11. package/dist/interactive-plugin.mjs +2 -2
  12. package/dist/{load-RosvaPZf.mjs → load--ovXqbNE.mjs} +286 -33
  13. package/dist/questions.mjs +1 -1
  14. package/dist/quiz-e2e.mjs +1 -1
  15. package/dist/quiz-plugin.mjs +3 -3
  16. package/dist/{render-D5kwkNt1.mjs → render-D3LsJY04.mjs} +2 -2
  17. package/dist/{render-DqHOJyHK.mjs → render-mYtzN1DV.mjs} +1 -1
  18. package/dist/{result-D_mmCVwN.mjs → result-DphIWHDF.mjs} +1 -1
  19. package/dist/{validate-Bxy9NfB8.mjs → validate-CfFykN0M.mjs} +1 -1
  20. package/dist/{validate-BDwL7Su3.mjs → validate-j3GjVhNF.mjs} +2 -2
  21. package/explainer/README.md +7 -3
  22. package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +26 -8
  23. package/explainer/docs/arc42/09-explainer-decisions.arc42.md +42 -0
  24. package/explainer/package.json +1 -1
  25. package/explainer/src/app.yaml +3 -1
  26. package/explainer/src/prozent/chapter.yaml +1 -1
  27. package/explainer/src/prozent/grundwert.yaml +2 -1
  28. package/explainer/src/prozent/prozentsatz.yaml +2 -1
  29. package/explainer/src/prozent/prozentwert.yaml +2 -1
  30. package/interactive/package.json +1 -1
  31. package/package.json +3 -4
  32. package/quiz/package.json +1 -1
  33. package/runtime/explainer/assessment.ts +4 -2
  34. package/runtime/explainer/check-page.ts +6 -2
  35. package/runtime/explainer/content.ts +20 -1
  36. package/runtime/explainer/exercises.ts +71 -6
  37. package/runtime/explainer/explainer.css +21 -0
  38. package/runtime/explainer/explainer.v1.schema.json +2 -2
  39. package/runtime/explainer/explainer.v2.schema.json +152 -0
  40. package/runtime/explainer/expression.ts +127 -1
  41. package/runtime/explainer/generated/texts.ts +98 -9
  42. package/runtime/explainer/generated/types.ts +8 -0
  43. package/runtime/explainer/load.ts +70 -16
  44. package/runtime/explainer/messages/de.json +5 -1
  45. package/runtime/explainer/messages/en.json +5 -1
  46. package/runtime/explainer/outline.ts +1 -1
  47. package/runtime/explainer/page-list.ts +38 -0
  48. package/runtime/explainer/plugin.ts +13 -4
  49. package/runtime/explainer/render.ts +26 -2
  50. package/runtime/explainer/schema.ts +92 -9
  51. package/runtime/explainer/tutorpage.ts +9 -1
  52. package/runtime/shared/blocks/blocks.v1.schema.json +1 -1
  53. package/runtime/shared/text/formula.ts +61 -1
  54. package/scaffold/explainer/src/app.yaml +5 -1
  55. package/scaffold/explainer/src/thema/chapter.yaml +1 -1
  56. package/scaffold/explainer/src/thema/einstieg.yaml +1 -1
@@ -1,15 +1,15 @@
1
1
  import { a as isLanguage, c as textLanguages, i as inLanguage, o as languageListProblems, r as LANGUAGES } from "./html-mffkHGHh.mjs";
2
- import { n as renderMaterial } from "./blocks-CxtCPAa5.mjs";
3
- import { a as inspect, c as renderMarkdown, i as formulaProblems, l as renderWithFormulas, o as plain, r as FORMULA, s as renderInline, u as renderFormula } from "./check-CteIFN96.mjs";
4
- import { n as checkMaterial } from "./validate-Bxy9NfB8.mjs";
2
+ import { n as renderMaterial } from "./blocks-D4SSGKuL.mjs";
3
+ import { a as inspect, c as renderMarkdown, i as formulaProblems, l as renderWithFormulas, o as plain, r as FORMULA, s as renderInline, u as renderFormula } from "./check-BxZOZPic.mjs";
4
+ import { n as checkMaterial } from "./validate-CfFykN0M.mjs";
5
5
  import { _ as questionIn, a as renderQuestion, b as rightAnswer, i as questionMessagesIn, n as answerText, o as renderSolution, u as isCorrect, v as questionTexts } from "./render-GS1Htlu8.mjs";
6
- import { _ as verdict, g as schemasById, h as knownProperties, i as addressProblem, m as question_v1_schema_default, s as questionDetails, t as QUESTION_BRANCH, u as questionSchemas } from "./validate-BDwL7Su3.mjs";
6
+ import { _ as verdict, g as schemasById, h as knownProperties, i as addressProblem, m as question_v1_schema_default, s as questionDetails, t as QUESTION_BRANCH, u as questionSchemas } from "./validate-j3GjVhNF.mjs";
7
7
  import { t as appName } from "./appname-Bh23cUuz.mjs";
8
8
  import { r as languageRoot } from "./pages-9RaaomoO.mjs";
9
- import { $ as parse, A as answerFor, B as label, D as generate, E as compiled, F as questionFor, G as taskText, I as taskKey, J as tokenize, K as templateProblems, L as answerLabel, M as exampleGiving, O as holds, P as pairValues, Q as evaluate, R as fill, T as StarvedError, W as stepsOf, X as kindOf, Y as unusedVariables, Z as ExpressionError, _ as termId, a as assessments, d as parseRef, et as same, g as isExercise, h as allBlocks, k as values, nt as Fraction, o as drawsTasks, q as templatesIn, s as exerciseOf, t as ASSESSMENT, tt as variables, w as message$1, z as hintOf } from "./assessment-Dy359O1O.mjs";
9
+ import { $ as writtenTemplates, A as items, B as chemPlaceholdersIn, D as compiled, E as StarvedError, H as hintOf, I as pairValues, J as taskText, L as questionFor, M as answerFor, O as generate, P as exampleGiving, Q as unusedVariables, R as taskKey, T as message$1, U as label, V as fill, X as templatesIn, Y as templateProblems, Z as tokenize, a as assessments, at as variables, d as parseRef, et as kindOf, g as isExercise, h as allBlocks, it as same, j as values, k as holds, nt as evaluate, o as drawsTasks, ot as Fraction, q as stepsOf, rt as parse, s as exerciseOf, t as ASSESSMENT, tt as ExpressionError, v as termId, z as answerLabel } from "./assessment-DaKsXUdj.mjs";
10
10
  import { a as RESERVED_PARAMS, l as distinctSeeds, o as SEED_TRIES } from "./tasks-DADUYy52.mjs";
11
11
  import { a as parse$1, i as nodes, o as readSfc, r as line, s as sfc, t as codeChecker } from "./vue-code-BkJjW_I_.mjs";
12
- import { a as lineOf, c as schemaProblems, i as isObject, n as Texts, o as pathText, r as get, s as readYaml, t as Problems } from "./content-DO6oEJxj.mjs";
12
+ import { a as lineOf, c as schemaProblems, i as isObject, n as Texts, o as pathText, r as get, s as readYaml, t as Problems } from "./content-CAL0Nz1a.mjs";
13
13
  import "node:module";
14
14
  import { Ajv2020 } from "ajv/dist/2020.js";
15
15
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
@@ -14738,16 +14738,67 @@ function formulaAlternative(text, lang) {
14738
14738
  return formulas.map((formula) => speakFormula(formula[1] ?? "", lang)).join(" ");
14739
14739
  }
14740
14740
  // runtime/explainer/exercises.ts
14741
- /** The templates of a generated exercise in each of `languages`; links inside them through `resolve`. */
14742
- function templatesOf(spec, languages, resolve = (href) => href) {
14741
+ /** A value of a placeholder in \ce{{{…}}} whose formula does not parse; the check reports its message as it is. */
14742
+ var ChemError = class extends Error {};
14743
+ /**
14744
+ * The chemical formula of each value of the placeholders in \ce{{{…}}} of an exercise's templates in `lang`, as
14745
+ * MathML without <math>, by the value as the learner reads it: each value of the list the placeholder's root is
14746
+ * drawn from (oneOf, or pick: 1 from a list or a data file), set by mhchem as \ce{value}. Throws a ChemError for a
14747
+ * value that does not parse.
14748
+ */
14749
+ function chemFormulas(spec, data, lang) {
14750
+ const formulas = {};
14751
+ const paths = new Set(writtenTemplates(spec).filter((written) => written.lang === void 0 || written.lang === lang).flatMap((written) => chemPlaceholdersIn(written.text)));
14752
+ for (const path of paths) {
14753
+ const root = path.split(".")[0] ?? "";
14754
+ const arbitrary = spec.given[root];
14755
+ if (arbitrary === void 0) continue;
14756
+ const source = arbitrary.oneOf ?? (arbitrary.pick === 1 ? arbitrary.from : void 0);
14757
+ if (source === void 0) continue;
14758
+ let candidates;
14759
+ try {
14760
+ candidates = items(source, data);
14761
+ } catch {
14762
+ continue;
14763
+ }
14764
+ for (const candidate of candidates) {
14765
+ let text;
14766
+ try {
14767
+ text = label(evaluate(parse(path), { [root]: candidate }), lang);
14768
+ } catch {
14769
+ continue;
14770
+ }
14771
+ if (text in formulas) continue;
14772
+ try {
14773
+ let depth = 0;
14774
+ for (const char of text) if (char === "{") depth++;
14775
+ else if (char === "}" && --depth < 0) break;
14776
+ if (depth !== 0) throw new Error("its braces are not balanced");
14777
+ const math = renderFormula(`\\ce{${text}}`, false);
14778
+ formulas[text] = /^<math[^>]*>([\s\S]*)<\/math>$/.exec(math)?.[1] ?? math;
14779
+ } catch (error) {
14780
+ const reason = (error instanceof Error ? error.message : String(error)).split("\n")[0]?.trim() ?? "";
14781
+ throw new ChemError(`\\ce{{{${path}}}}: the value "${text}" is no chemical formula mhchem can set (${reason}); write it in mhchem's notation, e.g. 2H2 + O2 -> 2H2O`);
14782
+ }
14783
+ }
14784
+ }
14785
+ return formulas;
14786
+ }
14787
+ /**
14788
+ * The templates of a generated exercise in each of `languages`, with the formulas of the values in \ce{{{…}}} drawn
14789
+ * from `data`; links inside them through `resolve`.
14790
+ */
14791
+ function templatesOf(spec, languages, data, resolve = (href) => href) {
14743
14792
  const templates = {};
14744
14793
  for (const lang of languages) {
14745
14794
  const inline = (text) => renderInline(tokenize(inLanguage(text, lang)), resolve);
14795
+ const chem = chemFormulas(spec, data, lang);
14746
14796
  templates[lang] = {
14747
14797
  task: inline(spec.task.text),
14748
14798
  steps: (spec.steps ?? []).map(inline),
14749
14799
  hint: spec.hint === void 0 ? void 0 : inline(spec.hint),
14750
- messages: (spec.mistakes ?? []).map((mistake) => tokenize(inLanguage(mistake.message, lang)))
14800
+ messages: (spec.mistakes ?? []).map((mistake) => tokenize(inLanguage(mistake.message, lang))),
14801
+ ...Object.keys(chem).length === 0 ? {} : { chem }
14751
14802
  };
14752
14803
  }
14753
14804
  return templates;
@@ -14765,7 +14816,7 @@ const seedCache = /* @__PURE__ */ new WeakMap();
14765
14816
  function taskSeeds(spec, data) {
14766
14817
  let seeds = seedCache.get(spec);
14767
14818
  if (seeds === void 0) {
14768
- const templates = templatesOf(spec, ["de"]);
14819
+ const templates = templatesOf(spec, ["de"], data);
14769
14820
  seeds = distinctSeeds((seed) => {
14770
14821
  try {
14771
14822
  return taskKey(spec, generate(spec, seed, data), "de", templates);
@@ -14844,7 +14895,7 @@ function generatedData(spec, lang, all, resolve) {
14844
14895
  kind: "generated",
14845
14896
  lang,
14846
14897
  spec: shipped,
14847
- templates: templatesOf(spec, [lang], resolve),
14898
+ templates: templatesOf(spec, [lang], data, resolve),
14848
14899
  data,
14849
14900
  seeds: taskSeeds(spec, data),
14850
14901
  texts: {
@@ -14907,8 +14958,8 @@ function openTaskExercise(block, lang, resolve, storage) {
14907
14958
  ].filter((part) => part !== "").join("\n");
14908
14959
  }
14909
14960
  function generatedExercise(spec, lang, all, resolve) {
14910
- const templates = templatesOf(spec, [lang], resolve);
14911
14961
  const data = dataOf(spec, all);
14962
+ const templates = templatesOf(spec, [lang], data, resolve);
14912
14963
  const solution = questionMessagesIn(lang).solution;
14913
14964
  const worked = workedTasks(spec, data, templates, lang).map((item) => [
14914
14965
  `<li class="explainer-worked-task">`,
@@ -15736,7 +15787,7 @@ var explainer_v1_schema_default = {
15736
15787
  "type": "object"
15737
15788
  },
15738
15789
  "expression": {
15739
- "description": "An expression of the expression language, e.g. \"G * p / 100\": numbers (exact fractions), + - * /, parentheses, == != < <= > >=, and, or, not, strings in quotes, record fields (e.year), list index (xs[0]), lists ([1, 2]), and the functions round(x, n), decimals, map(xs, field), sortBy(xs, field), reverse. It may use the variables of given.",
15790
+ "description": "An expression of the expression language, e.g. \"G * p / 100\": numbers (exact fractions), + - * /, parentheses, == != < <= > >=, and, or, not, strings in quotes, record fields (e.year), list index (xs[0]), lists ([1, 2]), and the functions round(x, n), decimals, map(xs, field), sortBy(xs, field), reverse, sqrt(x), pow(x, n), sin(x), cos(x), tan(x), asin(x), acos(x), atan(x). Angles are in degrees: sin(30) is 1/2. Round a result that is not rational (sqrt(2)) with round(x, n). It may use the variables of given.",
15740
15791
  "type": "string",
15741
15792
  "minLength": 1,
15742
15793
  "pattern": "\\S"
@@ -15854,7 +15905,7 @@ var explainer_v1_schema_default = {
15854
15905
  }
15855
15906
  },
15856
15907
  "generated": {
15857
- "description": "An exercise drawn from a seed, property-based: given draws the values, where filters them, task is the text with placeholders ({{G}}, {{e.name}}; {{answer}} in the steps), answer is an expression with its comparator, mistakes are typical wrong answers each with a message saying what went wrong, hint and steps help. A placeholder is always two braces around a declared name or its field, nothing else inside, in prose and in formulas alike ($\\frac{{{W}}}{{{G}}}$); an index is no field, so {{opts[0].x}} is no placeholder and the build reports it: draw the record on its own (given: { e: { pick: 1, from: { data: name } } }) and write {{e.x}}; one brace is never a placeholder, so in a formula G and {G} are the letter. The build checks that every placeholder names a variable of given (or answer), that no text has double braces around anything but a name or its field, that no prose has a placeholder with one brace or spaces inside, and that every variable of given is used in a placeholder or in an expression of the answer or mistakes; then it checks it over 200 fixed seeds: where holds in at least 1 of 10 draws; the answer is defined, in its format and passes its comparator; no mistake equals it; the task is the same each time and keeps no placeholder. Its tasks are numbered: the build walks the seeds 1, 2, 3, … and numbers only the distinct tasks, Aufgabe Nr. 1 to N (N at most 67; fewer when the exercise has fewer distinct tasks, noted in the build output). The number is what the page shows, what a learner names to a tutor, and what the address carries: ?seed=<n>#<id> opens task number n (a number above N wraps around). Without JavaScript the page shows the tasks 1, 2 and 3 as worked tasks with their solution; with JavaScript a live task of a random number, Neue Aufgabe, which moves to the next number (after N back to 1) and writes it into the address, and the deep link: a wrong answer gets the message of the mistake it matches (else a line to look again, with the hint above) and may be tried again; the solution and the steps show after the right answer or Lösung zeigen.",
15908
+ "description": "An exercise drawn from a seed, property-based: given draws the values, where filters them, task is the text with placeholders ({{G}}, {{e.name}}; {{answer}} in the steps), answer is an expression with its comparator, mistakes are typical wrong answers each with a message saying what went wrong, hint and steps help. A placeholder is always two braces around a declared name or its field, nothing else inside, in prose and in formulas alike ($\\frac{{{W}}}{{{G}}}$); an index is no field, so {{opts[0].x}} is no placeholder and the build reports it: draw the record on its own (given: { e: { pick: 1, from: { data: name } } }) and write {{e.x}}; one brace is never a placeholder, so in a formula G and {G} are the letter. A chemical formula from a record is $\\ce{{{r.gl}}}$: the whole formula in one field (gl: \"2H2 + O2 -> 2H2O\"), r drawn with pick: 1 or oneOf; a placeholder inside a longer \\ce or in \\pu is not possible. The build checks that every placeholder names a variable of given (or answer), that no text has double braces around anything but a name or its field, that no prose has a placeholder with one brace or spaces inside, and that every variable of given is used in a placeholder or in an expression of the answer or mistakes; then it checks it over 200 fixed seeds: where holds in at least 1 of 10 draws; the answer is defined, in its format and passes its comparator; no mistake equals it; the task is the same each time and keeps no placeholder. Its tasks are numbered: the build walks the seeds 1, 2, 3, … and numbers only the distinct tasks, Aufgabe Nr. 1 to N (N at most 67; fewer when the exercise has fewer distinct tasks, noted in the build output). The number is what the page shows, what a learner names to a tutor, and what the address carries: ?seed=<n>#<id> opens task number n (a number above N wraps around). Without JavaScript the page shows the tasks 1, 2 and 3 as worked tasks with their solution; with JavaScript a live task of a random number, Neue Aufgabe, which moves to the next number (after N back to 1) and writes it into the address, and the deep link: a wrong answer gets the message of the mistake it matches (else a line to look again, with the hint above) and may be tried again; the solution and the steps show after the right answer or Lösung zeigen.",
15858
15909
  "required": [
15859
15910
  "id",
15860
15911
  "given",
@@ -16096,10 +16147,125 @@ var explainer_v1_schema_default = {
16096
16147
  }
16097
16148
  }
16098
16149
  };
16150
+ // runtime/explainer/explainer.v2.schema.json
16151
+ var explainer_v2_schema_default = {
16152
+ $schema: "https://json-schema.org/draft/2020-12/schema",
16153
+ $id: "urn:lernapps:schema:explainer.v2",
16154
+ title: "Content of a lernapps explainer, format 2",
16155
+ description: "Format 2 of an explainer's content, as YAML in src/: src/app.yaml, src/<chapter>/chapter.yaml, src/<chapter>/<page>.yaml and src/data/<name>.yaml. It is format 1 (explainer.v1.schema.json) plus the app's place on the learning map: format: 2 and strand in src/app.yaml, and on each page the competencies it practises, or recap on a page that repeats a competency from elsewhere. An app.yaml without format is format 1.",
16156
+ allOf: [
16157
+ {
16158
+ "if": {
16159
+ "type": "object",
16160
+ "required": ["chapters"],
16161
+ "properties": { "chapters": {} }
16162
+ },
16163
+ "then": { "$ref": "#/$defs/app" }
16164
+ },
16165
+ {
16166
+ "if": {
16167
+ "type": "object",
16168
+ "required": ["pages"],
16169
+ "properties": { "pages": {} }
16170
+ },
16171
+ "then": { "$ref": "#/$defs/chapter" }
16172
+ },
16173
+ {
16174
+ "if": {
16175
+ "type": "object",
16176
+ "required": ["blocks"],
16177
+ "properties": { "blocks": {} }
16178
+ },
16179
+ "then": { "$ref": "#/$defs/page" }
16180
+ },
16181
+ {
16182
+ "if": { "type": "array" },
16183
+ "then": { "$ref": "#/$defs/data" }
16184
+ }
16185
+ ],
16186
+ $defs: {
16187
+ "id": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/id" },
16188
+ "strand": {
16189
+ "description": "The one strand (Strang) of the learning map the app belongs to, <subject>/<strand>, e.g. mathematik/stochastik; or none if it belongs to none (then it is not on the map).",
16190
+ "type": "string",
16191
+ "pattern": "^(none|[a-z0-9]+(-[a-z0-9]+)*/[a-z0-9]+(-[a-z0-9]+)*)$"
16192
+ },
16193
+ "competency": {
16194
+ "description": "A competency of the learning map, <subject>/<competency>, e.g. physik/gewichtskraft-berechnen.",
16195
+ "type": "string",
16196
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*/[a-z0-9]+(-[a-z0-9]+)*$"
16197
+ },
16198
+ "app": {
16199
+ "description": "src/app.yaml in format 2: format: 2 and strand, everything else as in format 1.",
16200
+ "type": "object",
16201
+ "additionalProperties": false,
16202
+ "required": [
16203
+ "format",
16204
+ "strand",
16205
+ "title",
16206
+ "intro",
16207
+ "languages",
16208
+ "chapters"
16209
+ ],
16210
+ "properties": {
16211
+ "format": {
16212
+ "description": "Always 2. Without it, the content is format 1 (explainer.v1.schema.json).",
16213
+ "const": 2
16214
+ },
16215
+ "strand": { "$ref": "#/$defs/strand" },
16216
+ "title": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/app/properties/title" },
16217
+ "intro": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/app/properties/intro" },
16218
+ "languages": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/app/properties/languages" },
16219
+ "chapters": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/app/properties/chapters" },
16220
+ "sources": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/app/properties/sources" },
16221
+ "tutor": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/app/properties/tutor" },
16222
+ "scope": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/app/properties/scope" }
16223
+ }
16224
+ },
16225
+ "chapter": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/chapter" },
16226
+ "page": {
16227
+ "description": "src/<chapter>/<page>.yaml in format 2: a page as in format 1, plus competencies, or recap on a recap page.",
16228
+ "type": "object",
16229
+ "additionalProperties": false,
16230
+ "required": ["title", "blocks"],
16231
+ "properties": {
16232
+ "title": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/page/properties/title" },
16233
+ "description": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/page/properties/description" },
16234
+ "competencies": {
16235
+ "description": "The competencies (Bausteine) the page practises, ids of the subject of the app's strand; as a rule one. Required when the app has a strand; leave it out with strand: none and on a recap page. The catalog checks the ids when it lists the app.",
16236
+ "type": "array",
16237
+ "minItems": 1,
16238
+ "uniqueItems": true,
16239
+ "items": { "$ref": "#/$defs/id" }
16240
+ },
16241
+ "recap": {
16242
+ "description": "Makes the page a recap page: it repeats a competency that the app's competencies require but the app does not practise, e.g. physik/gewichtskraft-berechnen. Use it instead of competencies, also with strand: none. A recap page has descriptive blocks only, no exercise; the link to the competency on the learning map is added for you.",
16243
+ "$ref": "#/$defs/competency"
16244
+ },
16245
+ "blocks": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/page/properties/blocks" },
16246
+ "scope": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/page/properties/scope" }
16247
+ },
16248
+ "not": {
16249
+ "required": ["competencies", "recap"],
16250
+ "properties": {
16251
+ "competencies": {},
16252
+ "recap": {}
16253
+ }
16254
+ }
16255
+ },
16256
+ "data": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/data" },
16257
+ "event": { "$ref": "urn:lernapps:schema:explainer.v1#/$defs/event" }
16258
+ }
16259
+ };
16099
16260
  // runtime/explainer/schema.ts
16100
16261
  const SCHEMA_ID = explainer_v1_schema_default.$id;
16101
- /** Where an app finds the schema of its content, for the message that names it. */
16102
- const SCHEMA_FILE = "node_modules/@lernapps/app-templates/runtime/explainer/explainer.v1.schema.json";
16262
+ const SCHEMA_ID_V2 = explainer_v2_schema_default.$id;
16263
+ /** Where an app finds the schema of its content, by format, for the message that names it. */
16264
+ const SCHEMA_FILES = {
16265
+ 1: "node_modules/@lernapps/app-templates/runtime/explainer/explainer.v1.schema.json",
16266
+ 2: "node_modules/@lernapps/app-templates/runtime/explainer/explainer.v2.schema.json"
16267
+ };
16268
+ SCHEMA_FILES[1];
16103
16269
  const ajv = new Ajv2020({
16104
16270
  allErrors: true,
16105
16271
  strict: true,
@@ -16107,9 +16273,10 @@ const ajv = new Ajv2020({
16107
16273
  });
16108
16274
  for (const each of questionSchemas) ajv.addSchema(each);
16109
16275
  ajv.addSchema(explainer_v1_schema_default);
16110
- const validator = (name) => {
16111
- const validate = ajv.getSchema(`${SCHEMA_ID}#/$defs/${name}`);
16112
- if (validate === void 0) throw new Error(`the schema has no $defs/${name}`);
16276
+ ajv.addSchema(explainer_v2_schema_default);
16277
+ const validator = (name, id = SCHEMA_ID) => {
16278
+ const validate = ajv.getSchema(`${id}#/$defs/${name}`);
16279
+ if (validate === void 0) throw new Error(`the schema ${id} has no $defs/${name}`);
16113
16280
  return validate;
16114
16281
  };
16115
16282
  const validators = {
@@ -16119,6 +16286,23 @@ const validators = {
16119
16286
  data: validator("data"),
16120
16287
  event: validator("event")
16121
16288
  };
16289
+ /** The validators of format 2: its own app and page; chapters, data files and events are those of format 1. */
16290
+ const validatorsV2 = {
16291
+ ...validators,
16292
+ app: validator("app", SCHEMA_ID_V2),
16293
+ page: validator("page", SCHEMA_ID_V2)
16294
+ };
16295
+ /** The format of the app `app` (src/app.yaml as read): 2 when it names any format, which its schema then checks. */
16296
+ const formatOf = (app) => isObject(app) && app["format"] !== void 0 ? 2 : 1;
16297
+ /** The fields of format 2, which format 1 does not know. */
16298
+ const FORMAT_2_FIELDS = /* @__PURE__ */ new Set([
16299
+ "format",
16300
+ "strand",
16301
+ "competencies",
16302
+ "recap"
16303
+ ]);
16304
+ /** The fields of format 2 on a page. */
16305
+ const PAGE_FIELDS = /* @__PURE__ */ new Set(["competencies", "recap"]);
16122
16306
  const schemas = schemasById(...questionSchemas, explainer_v1_schema_default);
16123
16307
  const BLOCK_DEFINITIONS = {
16124
16308
  text: "textBlock",
@@ -16147,6 +16331,7 @@ const questionFields = (type) => {
16147
16331
  /** What `required` names as missing, with why it is needed. */
16148
16332
  function missing(property, object) {
16149
16333
  const type = isObject(object) ? object["type"] : void 0;
16334
+ if (property === "strand") 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";
16150
16335
  if (property === "origin") return "origin is missing; a quote names who said or wrote it, where and when";
16151
16336
  if ((property === "licence" || property === "source") && type === "quote") return `${property} is missing; a quote names its source and its licence`;
16152
16337
  if (property === "licence" || property === "source") return `${property} is missing; a bundled image names its source and its licence`;
@@ -16163,7 +16348,35 @@ const describe = (error, path, value) => {
16163
16348
  ...lang === void 0 ? {} : { lang }
16164
16349
  };
16165
16350
  }
16351
+ case "not":
16352
+ if (path.length !== 0 || !isObject(value) || !("recap" in value) || !("competencies" in value)) return void 0;
16353
+ return {
16354
+ path: ["recap"],
16355
+ message: "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"
16356
+ };
16357
+ case "const":
16358
+ if (path.length !== 1 || path[0] !== "format") return void 0;
16359
+ return { message: `${String(value)} is not a format; write format: 2, or leave format out for format 1` };
16360
+ case "uniqueItems":
16361
+ if (path.at(-1) !== "competencies") return void 0;
16362
+ return { message: "names a competency twice; name each competency once" };
16363
+ case "additionalProperties": {
16364
+ const property = String(error.params["additionalProperty"]);
16365
+ if (path.length !== 0 || !FORMAT_2_FIELDS.has(property) || !isObject(value)) return void 0;
16366
+ if (PAGE_FIELDS.has(property) && "blocks" in value) return {
16367
+ path: [property],
16368
+ message: `${property} is a field of format 2; add format: 2 and the app's strand to src/app.yaml, or remove it`
16369
+ };
16370
+ if (!PAGE_FIELDS.has(property) && "chapters" in value) return {
16371
+ path: [property],
16372
+ message: `${property} is a field of format 2; add format: 2 to src/app.yaml, with the app's strand`
16373
+ };
16374
+ return;
16375
+ }
16166
16376
  case "pattern":
16377
+ if (path.length === 1 && path[0] === "strand") return { message: `${String(value)} is no strand; write <subject>/<strand> with the ids of the learning map, e.g. mathematik/stochastik, or none` };
16378
+ if (path.length === 1 && path[0] === "recap") return { 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` };
16379
+ if (path.at(-2) === "competencies") return { 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` };
16167
16380
  if (path.at(-2) === "exercises" && path[0] === "selfAssessment") return { message: `${String(value)} is no exercise's address; write /<chapter>/<page>/#<exercise>` };
16168
16381
  if (path.at(-1) !== "youtube") return void 0;
16169
16382
  return { message: `${String(value)} is no YouTube id; give the 11 characters after watch?v= in the video's address, e.g. rkLmLaFHodo` };
@@ -16186,8 +16399,8 @@ const describe = (error, path, value) => {
16186
16399
  };
16187
16400
  }
16188
16401
  };
16189
- function validate(yaml, kind, problems) {
16190
- const check = validators[kind];
16402
+ function validate(yaml, kind, problems, format = 1) {
16403
+ const check = (format === 2 ? validatorsV2 : validators)[kind];
16191
16404
  if (check(yaml.value)) return true;
16192
16405
  const before = problems.lines.length;
16193
16406
  schemaProblems(yaml, check.errors ?? [], problems, describe);
@@ -16604,8 +16817,12 @@ function checkGenerated(check, block, at) {
16604
16817
  for (const [name, file] of check.dataFiles) records[name] = file.value;
16605
16818
  let templates;
16606
16819
  try {
16607
- templates = templatesOf(block, texts.languages);
16820
+ templates = templatesOf(block, texts.languages, dataOf(block, records));
16608
16821
  } catch (error) {
16822
+ if (error instanceof ChemError) {
16823
+ problems.at(yaml, at, error.message);
16824
+ return;
16825
+ }
16609
16826
  const reason = (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
16610
16827
  problems.at(yaml, at, `a formula with its placeholders filled in does not parse (${reason.trim()}); check its braces: a placeholder is {{G}}, a letter is G`);
16611
16828
  return;
@@ -16708,11 +16925,11 @@ const IMAGE = /^[a-z0-9]+(-[a-z0-9]+)*\.(svg|png|jpg|webp)$/;
16708
16925
  /** The content could not be read: one problem per line in `problems`. */
16709
16926
  var ContentError = class extends Error {
16710
16927
  problems;
16711
- constructor(problems) {
16928
+ constructor(problems, format = 1) {
16712
16929
  super([
16713
16930
  `the explainer's content in src/ is not valid (${RULE_URL}):`,
16714
16931
  ...problems.map((problem) => ` - ${problem}`),
16715
- `Fix the content as its schema says: ${SCHEMA_FILE}`
16932
+ `Fix the content as its schema says: ${SCHEMA_FILES[format]}`
16716
16933
  ].join("\n"));
16717
16934
  this.problems = problems;
16718
16935
  }
@@ -16767,6 +16984,10 @@ function checkAssessment(yaml, assessment, chapters, chapter, components, proble
16767
16984
  problems.at(yaml, path, `${ref} names no page of the app; write /<chapter>/<page>/#<exercise>`);
16768
16985
  return;
16769
16986
  }
16987
+ if (page.recap !== void 0) {
16988
+ problems.at(yaml, path, `${ref} is on a recap page, which has no exercise and is left out of the self-assessment; choose an exercise of a page of the app's own competencies`);
16989
+ return;
16990
+ }
16770
16991
  const exercise = exerciseOf(components, page, parts.exercise);
16771
16992
  if (twice && exercise !== void 0 && !drawsTasks(exercise)) {
16772
16993
  problems.at(yaml, path, `${ref} is listed twice, and a question shows the same task each time; list it once (a generated exercise or an exercise component may be listed again: each listing draws a task of its own)`);
@@ -16778,13 +16999,27 @@ function checkAssessment(yaml, assessment, chapters, chapter, components, proble
16778
16999
  });
16779
17000
  });
16780
17001
  }
17002
+ /** Each block of `blocks` and the blocks inside them, with its path in the page's file. */
17003
+ function* blockPaths(blocks, path) {
17004
+ for (const [index, block] of blocks.entries()) {
17005
+ yield [block, [...path, index]];
17006
+ if (block.type === "section" || block.type === "example" || block.type === "note") yield* blockPaths(block.blocks, [
17007
+ ...path,
17008
+ index,
17009
+ "blocks"
17010
+ ]);
17011
+ }
17012
+ }
16781
17013
  /** The problems of the content in `root`/src, and the content when there are none. */
16782
17014
  function check(root, seeds = 200) {
16783
17015
  const problems = new Problems();
16784
17016
  const src = join(root, "src");
16785
17017
  if (!existsSync(join(src, "app.yaml"))) {
16786
17018
  problems.file("src/app.yaml", "is missing: the app's title, intro, languages and the order of its chapters belong there");
16787
- return { problems: problems.lines };
17019
+ return {
17020
+ problems: problems.lines,
17021
+ format: 1
17022
+ };
16788
17023
  }
16789
17024
  const chapterIds = [];
16790
17025
  const dataFiles = /* @__PURE__ */ new Map();
@@ -16811,11 +17046,18 @@ function check(root, seeds = 200) {
16811
17046
  const components = existsSync(join(src, "components")) ? readComponents(root) : void 0;
16812
17047
  for (const problem of components?.problems ?? []) problems.add(problem);
16813
17048
  const appYaml = readYaml(root, "src/app.yaml", problems);
16814
- if (appYaml === void 0 || !validate(appYaml, "app", problems)) return { problems: problems.lines };
17049
+ const format = formatOf(appYaml?.value);
17050
+ if (appYaml === void 0 || !validate(appYaml, "app", problems, format)) return {
17051
+ problems: problems.lines,
17052
+ format
17053
+ };
16815
17054
  const app = appYaml.value;
16816
17055
  const languageList = languageListProblems(app.languages, LANGUAGES);
16817
17056
  for (const problem of languageList) problems.at(appYaml, ["languages", Math.max(app.languages.lastIndexOf(problem.lang), 0)], problem.message);
16818
- if (languageList.length > 0) return { problems: problems.lines };
17057
+ if (languageList.length > 0) return {
17058
+ problems: problems.lines,
17059
+ format
17060
+ };
16819
17061
  const texts = new Texts(app.languages, problems);
16820
17062
  texts.check(appYaml, ["title"], "plain");
16821
17063
  texts.check(appYaml, ["intro"], "markdown");
@@ -16885,11 +17127,13 @@ function check(root, seeds = 200) {
16885
17127
  for (const pageId of pagesOk ? chapter.pages : pageIds) {
16886
17128
  const file = `src/${chapterId}/${pageId}.yaml`;
16887
17129
  const yaml = readYaml(root, file, problems);
16888
- if (yaml === void 0 || !validate(yaml, "page", problems)) {
17130
+ if (yaml === void 0 || !validate(yaml, "page", problems, format)) {
16889
17131
  complete = false;
16890
17132
  continue;
16891
17133
  }
16892
17134
  const page = yaml.value;
17135
+ if (app.strand === "none" && page.competencies !== void 0) problems.at(yaml, ["competencies"], "competencies is not allowed: src/app.yaml has strand: none, so the app is on no strand; remove it, or name the app's strand in src/app.yaml");
17136
+ else if (app.strand !== void 0 && app.strand !== "none" && page.competencies === void 0 && page.recap === void 0) problems.at(yaml, [], `competencies is missing; src/app.yaml names the strand ${app.strand}, so each page names the competencies of its subject that it practises, e.g. competencies: [laplace]; a page that only repeats a competency the app does not practise says recap: <subject>/<competency> instead`);
16893
17137
  texts.check(yaml, ["title"], "plain");
16894
17138
  texts.check(yaml, ["description"], "plain");
16895
17139
  const pageAnchors = /* @__PURE__ */ new Set();
@@ -16909,7 +17153,12 @@ function check(root, seeds = 200) {
16909
17153
  uses,
16910
17154
  dataUsed
16911
17155
  }, page.blocks, ["blocks"], 0);
16912
- if (![...allBlocks(page.blocks)].some(isExercise)) problems.at(yaml, ["blocks"], "the page has no exercise; give it a section with an exercise: one of the question types, or generated");
17156
+ if (page.recap !== void 0) for (const [block, path] of blockPaths(page.blocks, ["blocks"])) {
17157
+ const component = uses.find((use) => use.block === block && use.component.exercise);
17158
+ if (!isExercise(block) && component === void 0) continue;
17159
+ problems.at(yaml, path, `${component === void 0 ? `the exercise ${block.type}` : `the exercise component ${component.component.name}`} is not allowed on a recap page: it repeats ${page.recap} with descriptive blocks only, and the apps of that competency's strand practise it; remove the exercise, or make it a page of the app's own competencies`);
17160
+ }
17161
+ else if (![...allBlocks(page.blocks)].some(isExercise)) problems.at(yaml, ["blocks"], "the page has no exercise; give it a section with an exercise: one of the question types, or generated");
16913
17162
  for (const [id, path] of exercises) {
16914
17163
  if (pageAnchors.has(id)) problems.at(yaml, [...path, "id"], `${id} is also the anchor of a section of this page; give the exercise its own id`);
16915
17164
  pageAnchors.add(id);
@@ -16950,7 +17199,10 @@ function check(root, seeds = 200) {
16950
17199
  }
16951
17200
  for (const chapter of chapters) for (const page of chapter.pages) if (app.scope !== void 0 && page.scope === void 0) problems.file(page.file, "has no scope; src/app.yaml has one, so each page says in scope: what it teaches, in your own words from the material");
16952
17201
  else if (app.scope === void 0 && page.scope !== void 0) problems.file(page.file, "has a scope, but src/app.yaml has none; add scope: { out: [...] } to src/app.yaml, what does not belong to the scope");
16953
- if (problems.lines.length > 0) return { problems: problems.lines };
17202
+ if (problems.lines.length > 0) return {
17203
+ problems: problems.lines,
17204
+ format
17205
+ };
16954
17206
  const data = {};
16955
17207
  for (const [id, yaml] of dataFiles) data[id] = yaml.value;
16956
17208
  return {
@@ -16962,15 +17214,16 @@ function check(root, seeds = 200) {
16962
17214
  components: uses,
16963
17215
  storage: appName(root, "explainer")
16964
17216
  },
16965
- problems: []
17217
+ problems: [],
17218
+ format
16966
17219
  };
16967
17220
  }
16968
17221
  /**
16969
17222
  * The content in `root`/src, its generated exercises tested over `seeds` seeds (0: not at all, for a reader of content
16970
17223
  * the build has checked); throws a ContentError with every problem when it is not valid. */
16971
17224
  function loadSite(root, seeds = 200) {
16972
- const { site, problems } = check(root, seeds);
16973
- if (site === void 0) throw new ContentError(problems);
17225
+ const { site, problems, format } = check(root, seeds);
17226
+ if (site === void 0) throw new ContentError(problems, format);
16974
17227
  return site;
16975
17228
  }
16976
17229
  export { exampleProblems$1 as _, routes as a, exerciseData as c, templatesOf as d, workedTasks as f, describedProblems as g, TABLE_CONTRACT as h, hiddenChapter as i, renderExercise as l, SIZE_BUDGET as m, loadSite as n, terms as o, formulaAlternative as p, contentPages as r, dataOf as s, ContentError as t, taskSeeds as u, propertyProblems as v, propsIn as y };
@@ -1,5 +1,5 @@
1
1
  import { C as wrongAnswer, S as shuffled, T as de_default, _ as questionIn, a as renderQuestion, b as rightAnswer, c as hash, d as materialPairs, f as missesRight, g as questionId, h as questionAddresses, i as questionMessagesIn, l as isComplete, m as parseNumber, n as answerText, o as renderSolution, p as mixed, s as QUESTION_TYPES, t as QUESTION_MESSAGES, u as isCorrect, v as questionTexts, w as wrongReasons, x as rightEntries, y as random } from "./render-GS1Htlu8.mjs";
2
- import { c as questionSchema, l as questionSchemaProblems, n as QUESTION_REF, p as validateQuestion, r as QUESTION_SCHEMA_ID, s as questionDetails, u as questionSchemas } from "./validate-BDwL7Su3.mjs";
2
+ import { c as questionSchema, l as questionSchemaProblems, n as QUESTION_REF, p as validateQuestion, r as QUESTION_SCHEMA_ID, s as questionDetails, u as questionSchemas } from "./validate-j3GjVhNF.mjs";
3
3
  // runtime/shared/questions/index.ts
4
4
  /** The texts of a question for learners in German, by their key (messages/de.json); questionMessagesIn(lang) for another. */
5
5
  const messages = de_default;
package/dist/quiz-e2e.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { C as wrongAnswer, b as rightAnswer, g as questionId } from "./render-GS1Htlu8.mjs";
2
2
  import { i as expectMaterial, n as expectBackground, o as question, r as expectFeedbackInView, t as answerQuestion } from "./e2e-CU0n_unV.mjs";
3
3
  import { n as quizLanguages, t as quizIn } from "./quiz-DnW-xtCh.mjs";
4
- import { n as messagesIn, t as QUIZ_MESSAGES } from "./render-DqHOJyHK.mjs";
4
+ import { n as messagesIn, t as QUIZ_MESSAGES } from "./render-mYtzN1DV.mjs";
5
5
  import { expect, test } from "playwright/test";
6
6
  import { readFileSync } from "node:fs";
7
7
  // runtime/quiz/e2e.ts
@@ -1,12 +1,12 @@
1
1
  import { c as textLanguages, o as languageListProblems, r as LANGUAGES, t as escape } from "./html-mffkHGHh.mjs";
2
2
  import { r as materialImages } from "./block-BK_ohsH1.mjs";
3
- import "./blocks-CxtCPAa5.mjs";
4
- import { a as duplicates, d as readable, f as schemaMessages, o as inQuestion, p as validateQuestion, u as questionSchemas } from "./validate-BDwL7Su3.mjs";
3
+ import "./blocks-D4SSGKuL.mjs";
4
+ import { a as duplicates, d as readable, f as schemaMessages, o as inQuestion, p as validateQuestion, u as questionSchemas } from "./validate-j3GjVhNF.mjs";
5
5
  import { n as quizLanguages, t as quizIn } from "./quiz-DnW-xtCh.mjs";
6
6
  import { t as appName } from "./appname-Bh23cUuz.mjs";
7
7
  import { n as isSizeless, t as imageType } from "./image-CspJVfyQ.mjs";
8
8
  import { c as withLanguage, i as notFoundPage, n as fromDepth, r as languageRoot } from "./pages-9RaaomoO.mjs";
9
- import { r as renderQuiz } from "./render-DqHOJyHK.mjs";
9
+ import { r as renderQuiz } from "./render-mYtzN1DV.mjs";
10
10
  import { Ajv2020 } from "ajv/dist/2020.js";
11
11
  import { existsSync, readFileSync } from "node:fs";
12
12
  import { join } from "node:path";
@@ -1,7 +1,7 @@
1
1
  import { i as inLanguage, l as textsIn, o as languageListProblems, r as LANGUAGES } from "./html-mffkHGHh.mjs";
2
- import { c as renderMarkdown, o as plain } from "./check-CteIFN96.mjs";
2
+ import { c as renderMarkdown, o as plain } from "./check-BxZOZPic.mjs";
3
3
  import { a as pageFooter, o as pageHeader, r as languageRoot, s as relative, t as alternateLinks } from "./pages-9RaaomoO.mjs";
4
- import { c as schemaProblems, n as Texts, s as readYaml, t as Problems } from "./content-DO6oEJxj.mjs";
4
+ import { c as schemaProblems, n as Texts, s as readYaml, t as Problems } from "./content-CAL0Nz1a.mjs";
5
5
  import { Ajv2020 } from "ajv/dist/2020.js";
6
6
  import { existsSync, readdirSync } from "node:fs";
7
7
  import { join } from "node:path";
@@ -1,5 +1,5 @@
1
1
  import { l as textsIn, s as localeOf, t as escape } from "./html-mffkHGHh.mjs";
2
- import { n as renderMaterial } from "./blocks-CxtCPAa5.mjs";
2
+ import { n as renderMaterial } from "./blocks-D4SSGKuL.mjs";
3
3
  import { a as renderQuestion, g as questionId, o as renderSolution, r as external, t as QUESTION_MESSAGES } from "./render-GS1Htlu8.mjs";
4
4
  import { a as de_default, i as en_default, r as solutionId } from "./quiz-DnW-xtCh.mjs";
5
5
  import { a as pageFooter, o as pageHeader, r as languageRoot, s as relative, t as alternateLinks } from "./pages-9RaaomoO.mjs";
@@ -1,5 +1,5 @@
1
1
  import { x as rightEntries } from "./render-GS1Htlu8.mjs";
2
- import { w as message } from "./assessment-Dy359O1O.mjs";
2
+ import { T as message } from "./assessment-DaKsXUdj.mjs";
3
3
  // runtime/explainer/result.ts
4
4
  const key = (id, n) => n === void 0 ? id : `${id}=${n}`;
5
5
  /**
@@ -1,6 +1,6 @@
1
1
  import { i as inLanguage } from "./html-mffkHGHh.mjs";
2
2
  import { n as markerOf } from "./block-BK_ohsH1.mjs";
3
- import { t as checkText } from "./check-CteIFN96.mjs";
3
+ import { t as checkText } from "./check-BxZOZPic.mjs";
4
4
  // runtime/shared/blocks/validate.ts
5
5
  /** The definition of each type of block in the schema of the blocks (blocks.v1.schema.json). */
6
6
  const MATERIAL_DEFINITIONS = {
@@ -1,5 +1,5 @@
1
1
  import { c as textLanguages } from "./html-mffkHGHh.mjs";
2
- import { n as checkMaterial, t as MATERIAL_DEFINITIONS } from "./validate-Bxy9NfB8.mjs";
2
+ import { n as checkMaterial, t as MATERIAL_DEFINITIONS } from "./validate-CfFykN0M.mjs";
3
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
4
  import { Ajv2020 } from "ajv/dist/2020.js";
5
5
  // runtime/shared/examples.ts
@@ -298,7 +298,7 @@ var blocks_v1_schema_default = {
298
298
  "type": "object"
299
299
  },
300
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.",
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), \\ (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.",
302
302
  "$ref": "urn:lernapps:schema:text.v1#/$defs/text"
303
303
  },
304
304
  "table": {
@@ -11,7 +11,8 @@ Chapters of pages that explain a topic, with exercises on every page. Every page
11
11
  videos directly from then on. The catalog entry of an app with a video says `fitness.thirdParty: on-click`.
12
12
  - Exercises of the six question types, with feedback, hints and typical mistakes; a question can show content (`material`: the figures, tables, quotes and code of the page's blocks) between its text and its answers.
13
13
  - Generated exercises: each task draws new numbers or records, up to 67 numbered tasks per exercise, with typical
14
- mistakes, a hint and the way to the solution. The build tests them over fixed seeds.
14
+ mistakes, a hint and the way to the solution, also with roots, powers and trigonometry, and with chemical formulas
15
+ from a data file. The build tests them over fixed seeds.
15
16
  - Vue components for what no block can show or check, such as reading a scale, labelling a figure or dragging into
16
17
  place; an exercise component draws its own tasks. A component needs its logic module, a test and a text for readers
17
18
  without JavaScript. An exercise component also declares its exercise for the tutor (`template`, `params`,
@@ -41,7 +42,7 @@ Chapters of pages that explain a topic, with exercises on every page. Every page
41
42
  - No timer, no game.
42
43
  - At most two section levels in a page.
43
44
  - An exercise component does not count as a page's required exercise: each page also needs a question or a generated
44
- exercise.
45
+ exercise. A recap page has none at all.
45
46
  - Stores nothing but the learner's choice of AI tutor, if they make it, the choice to load videos directly, and the
46
47
  last result of each self-assessment they do.
47
48
  - A self-assessment needs JavaScript (without it, it is not offered).
@@ -50,5 +51,8 @@ Chapters of pages that explain a topic, with exercises on every page. Every page
50
51
  ## What the app supplies
51
52
 
52
53
  - The content as YAML in `src/`: the app, a folder per chapter with its pages, data files and images
53
- ([schema](../runtime/explainer/explainer.v1.schema.json)); Vue components in `src/components/` where
54
+ ([schema](../runtime/explainer/explainer.v2.schema.json), format 2); Vue components in `src/components/` where
54
55
  no block fits.
56
+ - Its place on the catalog's learning map (format 2): the app's strand, and the competencies each page practises. A
57
+ recap page repeats a competency from elsewhere that the app needs, without exercises. Format 1
58
+ ([schema](../runtime/explainer/explainer.v1.schema.json)) stays valid, without a place on the map.