@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
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ feature: assistants read it before they choose an archetype.
|
|
|
21
21
|
| Folder | Archetype | What the app supplies |
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| [`quiz/`](quiz/) | a quiz about things to know: questions with options, feedback on every option, score and solutions, in one or more languages (a page per language) | the question bank `src/quiz.json` ([schema](runtime/quiz/quiz.v1.schema.json)) |
|
|
24
|
-
| [`explainer/`](explainer/) | an explainer: chapters of pages with sections, formulas, figures, tables, timelines, definitions with a glossary, quotes, code and YouTube videos behind two clicks, and an exercise on every page (the quiz's question types, and `generated` exercises drawn from a seed and property-tested at build), readable without JavaScript, in one or more languages; an optional self-assessment of the app or a chapter, checked as a whole; Vue components for what no block can express | the content as YAML in `src/`: `app.yaml`, a folder per chapter with `chapter.yaml` and its pages, `data/` and `images/`, and `components/` ([schema](runtime/explainer/explainer.v1.schema.json)) |
|
|
24
|
+
| [`explainer/`](explainer/) | an explainer: chapters of pages with sections, formulas, figures, tables, timelines, definitions with a glossary, quotes, code and YouTube videos behind two clicks, and an exercise on every page but a recap page (the quiz's question types, and `generated` exercises drawn from a seed and property-tested at build), readable without JavaScript, in one or more languages; an optional self-assessment of the app or a chapter, checked as a whole; Vue components for what no block can express | the content as YAML in `src/`: `app.yaml`, a folder per chapter with `chapter.yaml` and its pages, `data/` and `images/`, and `components/` ([schema](runtime/explainer/explainer.v2.schema.json); format 1: [schema](runtime/explainer/explainer.v1.schema.json)) |
|
|
25
25
|
| [`interactive/`](interactive/) | an interactive app whose doing is the content (a game, a trainer): a static start page, readable without JavaScript, and an app page with the app's own Vue components, its routes as `app/#<id>`; the build plays its playthroughs against its core | `src/app.yaml` ([schema](runtime/interactive/interactive.v1.schema.json)), the core `src/core/index.ts` (`start(seed, route)`, `step(state, input)` with a key or an action of its root component, optional `tick(state, dt)`), its components in `src/ui/`, data in `src/data/`, and `src/playthroughs/*.yaml` ([schema](runtime/interactive/playthrough.v1.schema.json)) |
|
|
26
26
|
|
|
27
27
|
- [`runtime/`](runtime/): the runtime of each archetype, published to npm as `@lernapps/app-templates` (with the template folders, complete example apps, and the scaffold of each archetype: `lernapps create` of `@lernapps/tooling` copies the paths `lernapps.scaffold.<archetype>` in [`package.json`](package.json) lists and the placeholder in [`scaffold/<archetype>/`](scaffold/), and leaves the example in the installed package; [`scripts/scaffold.ts`](scripts/scaffold.ts) writes the same for the CI job `scaffold`); its exports
|
|
@@ -29,14 +29,8 @@ feature: assistants read it before they choose an archetype.
|
|
|
29
29
|
one by one, for other archetypes that embed them as exercises: `@lernapps/app-templates/quiz/question` (schema,
|
|
30
30
|
validation, rendering, checker, messages; a question's texts may be maps by language), `startQuestion` of `@lernapps/app-templates/quiz` and the helpers of
|
|
31
31
|
`@lernapps/app-templates/quiz/e2e`; [`test/fixtures/question-page/`](test/fixtures/question-page/) is a page built so.
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
(`$\frac{{{W}}}{{{G}}} \cdot 100 = {{answer}}$`). One brace is never a placeholder, so in a formula `G`, `{G}` and
|
|
35
|
-
`\frac{W}{G}` are letters; the only LaTeX form this reserves, a double group around a lone name, means the same as
|
|
36
|
-
the name alone. The build fails on a placeholder whose name `given` does not declare, on one brace or spaces where a
|
|
37
|
-
placeholder was meant in prose (`{G}`, `{{ G }}`), on a variable of `given` that nothing uses (no placeholder, and
|
|
38
|
-
no expression of the figure, answer or mistakes; `where` does not count), and on a placeholder left after filling in
|
|
39
|
-
([`generated.ts`](runtime/explainer/generated.ts)).
|
|
32
|
+
- How to write the explainer's `generated` exercises, with their placeholders, is in its
|
|
33
|
+
[schema](runtime/explainer/explainer.v1.schema.json) (`$defs/generated`).
|
|
40
34
|
- An explainer's components are Vue single-file components in `src/components/<Name>.vue`, each with
|
|
41
35
|
`<script setup lang="ts">`, typed props and its test `<Name>.test.ts`, used by a `vue-component` block with its
|
|
42
36
|
`props` and a `nojs` text. The build renders each with Vue's server renderer into its island, `hidden` until it
|
|
@@ -188,8 +182,8 @@ positive list, `TEMPLATE_PATHS`: a path on no list runs the root checks only, an
|
|
|
188
182
|
change scope and runs all of them for any code change, catches the rest; when it catches what the hook let through, add
|
|
189
183
|
the path to the list. The fixture jobs run only in CI. The job `check` reports the overall result and is the check to
|
|
190
184
|
require. A change reaches new apps once it is released ([`release.yml`](.github/workflows/release.yml): after a merge to `main`,
|
|
191
|
-
semantic-release takes the version from the conventional commits, tags `v<version>` and publishes to npm with provenance) and lernapps/tooling depends on that version; Renovate moves existing apps.
|
|
192
|
-
|
|
185
|
+
semantic-release takes the version from the conventional commits, tags `v<version>` and publishes to npm with provenance) and lernapps/tooling depends on that version; Renovate moves existing apps. Each template folder has
|
|
186
|
+
its `.gitignore` also as `_gitignore`, because npm never ships a `.gitignore`; change both together.
|
|
193
187
|
|
|
194
188
|
## License
|
|
195
189
|
|
|
@@ -123,7 +123,15 @@ const FUNCTIONS = {
|
|
|
123
123
|
arity: 2,
|
|
124
124
|
field: 1
|
|
125
125
|
},
|
|
126
|
-
reverse: { arity: 1 }
|
|
126
|
+
reverse: { arity: 1 },
|
|
127
|
+
sqrt: { arity: 1 },
|
|
128
|
+
pow: { arity: 2 },
|
|
129
|
+
sin: { arity: 1 },
|
|
130
|
+
cos: { arity: 1 },
|
|
131
|
+
tan: { arity: 1 },
|
|
132
|
+
asin: { arity: 1 },
|
|
133
|
+
acos: { arity: 1 },
|
|
134
|
+
atan: { arity: 1 }
|
|
127
135
|
};
|
|
128
136
|
const KEYWORDS = /* @__PURE__ */ new Set([
|
|
129
137
|
"and",
|
|
@@ -444,6 +452,98 @@ function integer(value, what) {
|
|
|
444
452
|
if (!value.isInteger()) throw new ExpressionError(`${what} needs a whole number`);
|
|
445
453
|
return Number(value.n);
|
|
446
454
|
}
|
|
455
|
+
/** A real number that is no fraction, as the fraction of its first APPROXIMATION_PLACES decimals. */
|
|
456
|
+
function approximate(value, what) {
|
|
457
|
+
if (!Number.isFinite(value)) throw new ExpressionError(`${what} has no result here`);
|
|
458
|
+
return Fraction.from(Number(value.toFixed(12)));
|
|
459
|
+
}
|
|
460
|
+
/** The square root of a whole number ≥ 0, if it is whole. */
|
|
461
|
+
function wholeRoot(n) {
|
|
462
|
+
if (n < 2n) return n;
|
|
463
|
+
let x = BigInt(Math.floor(Math.sqrt(Number(n))));
|
|
464
|
+
while (x * x > n) x--;
|
|
465
|
+
while ((x + 1n) * (x + 1n) <= n) x++;
|
|
466
|
+
return x * x === n ? x : void 0;
|
|
467
|
+
}
|
|
468
|
+
function sqrt(x) {
|
|
469
|
+
if (x.n < 0n) throw new ExpressionError(`sqrt needs a number ≥ 0, not ${x.toNumber()}`);
|
|
470
|
+
const n = wholeRoot(x.n);
|
|
471
|
+
const d = wholeRoot(x.d);
|
|
472
|
+
return n !== void 0 && d !== void 0 ? Fraction.of(n, d) : approximate(Math.sqrt(x.toNumber()), "sqrt");
|
|
473
|
+
}
|
|
474
|
+
/** The largest exponent of pow: beyond it, a number has more digits than any exercise shows. */
|
|
475
|
+
const MAX_EXPONENT = 1e3;
|
|
476
|
+
function pow(x, n) {
|
|
477
|
+
if (n.isInteger()) {
|
|
478
|
+
const e = Number(n.n);
|
|
479
|
+
if (Math.abs(e) > MAX_EXPONENT) throw new ExpressionError(`pow takes an exponent from -${MAX_EXPONENT} to ${MAX_EXPONENT}`);
|
|
480
|
+
if (x.n === 0n && e < 0) throw new ExpressionError("pow(0, n) needs n ≥ 0: division by zero");
|
|
481
|
+
const power = Fraction.of(x.n ** BigInt(Math.abs(e)), x.d ** BigInt(Math.abs(e)));
|
|
482
|
+
return e < 0 ? Fraction.of(1).div(power) : power;
|
|
483
|
+
}
|
|
484
|
+
if (x.n < 0n) throw new ExpressionError("pow of a negative number needs a whole exponent");
|
|
485
|
+
return approximate(x.toNumber() ** n.toNumber(), "pow");
|
|
486
|
+
}
|
|
487
|
+
/** The angle `x` in degrees, brought into 0 ≤ x < 360 exactly. */
|
|
488
|
+
function turn(x) {
|
|
489
|
+
const full = Fraction.of(360);
|
|
490
|
+
const quotient = x.div(full);
|
|
491
|
+
let floor = quotient.n / quotient.d;
|
|
492
|
+
if (quotient.n < 0n && quotient.n % quotient.d !== 0n) floor -= 1n;
|
|
493
|
+
return x.sub(full.mul(Fraction.of(floor)));
|
|
494
|
+
}
|
|
495
|
+
/** The sine of the angles 0°, 30°, 90°, … whose sine is rational, in degrees. */
|
|
496
|
+
const EXACT_SINE = /* @__PURE__ */ new Map([
|
|
497
|
+
[0, Fraction.of(0)],
|
|
498
|
+
[30, Fraction.of(1, 2)],
|
|
499
|
+
[90, Fraction.of(1)],
|
|
500
|
+
[150, Fraction.of(1, 2)],
|
|
501
|
+
[180, Fraction.of(0)],
|
|
502
|
+
[210, Fraction.of(-1, 2)],
|
|
503
|
+
[270, Fraction.of(-1)],
|
|
504
|
+
[330, Fraction.of(-1, 2)]
|
|
505
|
+
]);
|
|
506
|
+
const RADIANS = Math.PI / 180;
|
|
507
|
+
function sine(degrees) {
|
|
508
|
+
const angle = turn(degrees);
|
|
509
|
+
return (angle.isInteger() ? EXACT_SINE.get(Number(angle.n)) : void 0) ?? approximate(Math.sin(angle.toNumber() * RADIANS), "sin");
|
|
510
|
+
}
|
|
511
|
+
const cosine = (degrees) => sine(degrees.add(Fraction.of(90)));
|
|
512
|
+
function tangent(degrees) {
|
|
513
|
+
const angle = turn(degrees);
|
|
514
|
+
if (cosine(angle).n === 0n) throw new ExpressionError(`tan(${degrees.toNumber()}) has no value: the cosine of the angle is 0`);
|
|
515
|
+
if (angle.isInteger() && [
|
|
516
|
+
45,
|
|
517
|
+
135,
|
|
518
|
+
225,
|
|
519
|
+
315
|
|
520
|
+
].includes(Number(angle.n))) return Fraction.of(Number(angle.n) % 180 === 45 ? 1 : -1);
|
|
521
|
+
const sin = sine(angle);
|
|
522
|
+
return sin.n === 0n ? sin : approximate(Math.tan(angle.toNumber() * RADIANS), "tan");
|
|
523
|
+
}
|
|
524
|
+
/** The inverse of sin, cos or tan in degrees: exact for the values of the angles of EXACT_SINE and of 45°. */
|
|
525
|
+
function inverse(fn, x) {
|
|
526
|
+
if (fn !== "atan" && (x.compare(Fraction.of(-1)) < 0 || x.compare(Fraction.of(1)) > 0)) throw new ExpressionError(`${fn} needs a number from -1 to 1, not ${x.toNumber()}`);
|
|
527
|
+
const known = (fn === "asin" ? {
|
|
528
|
+
"-1": -90,
|
|
529
|
+
"-1/2": -30,
|
|
530
|
+
"0": 0,
|
|
531
|
+
"1/2": 30,
|
|
532
|
+
"1": 90
|
|
533
|
+
} : fn === "acos" ? {
|
|
534
|
+
"-1": 180,
|
|
535
|
+
"-1/2": 120,
|
|
536
|
+
"0": 90,
|
|
537
|
+
"1/2": 60,
|
|
538
|
+
"1": 0
|
|
539
|
+
} : {
|
|
540
|
+
"-1": -45,
|
|
541
|
+
"0": 0,
|
|
542
|
+
"1": 45
|
|
543
|
+
})[x.toString()];
|
|
544
|
+
if (known !== void 0) return Fraction.of(known);
|
|
545
|
+
return approximate((fn === "asin" ? Math.asin(x.toNumber()) : fn === "acos" ? Math.acos(x.toNumber()) : Math.atan(x.toNumber())) / RADIANS, fn);
|
|
546
|
+
}
|
|
447
547
|
function call(fn, args, env) {
|
|
448
548
|
const value = (index) => evaluate(args[index], env);
|
|
449
549
|
const field = () => args[1].name;
|
|
@@ -459,6 +559,14 @@ function call(fn, args, env) {
|
|
|
459
559
|
return [...asList(value(0), "sortBy")].sort((a, b) => order(fieldOf(a, name), fieldOf(b, name), "sortBy"));
|
|
460
560
|
}
|
|
461
561
|
case "reverse": return [...asList(value(0), "reverse")].reverse();
|
|
562
|
+
case "sqrt": return sqrt(asNumber(value(0), "sqrt"));
|
|
563
|
+
case "pow": return pow(asNumber(value(0), "pow"), asNumber(value(1), "pow"));
|
|
564
|
+
case "sin": return sine(asNumber(value(0), "sin"));
|
|
565
|
+
case "cos": return cosine(asNumber(value(0), "cos"));
|
|
566
|
+
case "tan": return tangent(asNumber(value(0), "tan"));
|
|
567
|
+
case "asin":
|
|
568
|
+
case "acos":
|
|
569
|
+
case "atan": return inverse(fn, asNumber(value(0), fn));
|
|
462
570
|
default: throw new ExpressionError(`${fn} is not a function of the expression language`);
|
|
463
571
|
}
|
|
464
572
|
}
|
|
@@ -553,6 +661,10 @@ const PATH_ONLY = new RegExp(String.raw`^${PATH}$`);
|
|
|
553
661
|
const SPACED = new RegExp(String.raw`\{\{\s*(${PATH})\s*\}\}`, "g");
|
|
554
662
|
/** A formula ($…$) or a run of prose, in a template. */
|
|
555
663
|
const SEGMENT = /\$((?:\\.|[^\\$\n])+?)\$|([^$]+)/g;
|
|
664
|
+
/** A chemical formula whose whole argument is one placeholder: \ce{{{e.gl}}}. */
|
|
665
|
+
const CHEM = new RegExp(String.raw`\\ce\{\{\{(${PATH})\}\}\}`, "g");
|
|
666
|
+
/** The start of the token of a placeholder that is the whole argument of \ce: ⟦ce:e.gl⟧. */
|
|
667
|
+
const CHEM_TOKEN = "ce:";
|
|
556
668
|
const formats = /* @__PURE__ */ new Map();
|
|
557
669
|
/** A number as the page shows it: up to six decimals, no grouping (a learner would type 1500, not 1.500). */
|
|
558
670
|
function formatNumber(value, lang, decimals) {
|
|
@@ -583,9 +695,61 @@ function answerLabel(spec, task, lang) {
|
|
|
583
695
|
if (kind.kind === "number" && task.answer instanceof Fraction) return formatNumber(task.answer, lang, kind.money && !task.answer.isInteger() ? 2 : void 0);
|
|
584
696
|
return label(task.answer, lang);
|
|
585
697
|
}
|
|
586
|
-
/**
|
|
698
|
+
/**
|
|
699
|
+
* A template's placeholders as tokens: {{G}} becomes ⟦G⟧, inside a formula ($…$) {\text{⟦G⟧}}; a chemical formula
|
|
700
|
+
* whose whole argument is a placeholder, \ce{{{e.gl}}}, becomes {\text{⟦ce:e.gl⟧}}, which fill() replaces by the
|
|
701
|
+
* formula the build set for the task's value (LanguageTemplates.chem).
|
|
702
|
+
*/
|
|
587
703
|
function tokenize(text) {
|
|
588
|
-
return text.replace(SEGMENT, (whole, math, prose) => math !== void 0 ? `$${math.replace(PLACEHOLDER, (_placeholder, path) => `{\\text{⟦${path}⟧}}`)}$` : (prose ?? whole).replace(PLACEHOLDER, (_placeholder, path) => `⟦${path}⟧`));
|
|
704
|
+
return text.replace(SEGMENT, (whole, math, prose) => math !== void 0 ? `$${math.replace(CHEM, (_chem, path) => `{\\text{⟦${CHEM_TOKEN}${path}⟧}}`).replace(PLACEHOLDER, (_placeholder, path) => `{\\text{⟦${path}⟧}}`)}$` : (prose ?? whole).replace(PLACEHOLDER, (_placeholder, path) => `⟦${path}⟧`));
|
|
705
|
+
}
|
|
706
|
+
/** The formulas of a template ($…$), as written. */
|
|
707
|
+
const formulasIn = (text) => [...text.matchAll(SEGMENT)].flatMap((match) => match[1] === void 0 ? [] : [match[1]]);
|
|
708
|
+
/** The placeholders that are the whole argument of a \ce in a formula of a template (\ce{{{e.gl}}}), by their path. */
|
|
709
|
+
function chemPlaceholdersIn(text) {
|
|
710
|
+
return formulasIn(text).flatMap((math) => [...math.matchAll(CHEM)].map((match) => match[1] ?? ""));
|
|
711
|
+
}
|
|
712
|
+
/** The argument of each \ce and \pu of a formula, with its command; braces balanced. */
|
|
713
|
+
function mhchemArguments(math) {
|
|
714
|
+
const found = [];
|
|
715
|
+
for (const match of math.matchAll(/\\(ce|pu)\s*\{/g)) {
|
|
716
|
+
const start = (match.index ?? 0) + match[0].length;
|
|
717
|
+
let depth = 1;
|
|
718
|
+
for (let at = start; at < math.length; at++) if (math[at] === "{") depth++;
|
|
719
|
+
else if (math[at] === "}" && --depth === 0) {
|
|
720
|
+
found.push({
|
|
721
|
+
command: `\\${match[1] ?? ""}`,
|
|
722
|
+
argument: math.slice(start, at)
|
|
723
|
+
});
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return found;
|
|
728
|
+
}
|
|
729
|
+
const WHOLE_PLACEHOLDER = new RegExp(String.raw`^\{\{${PATH}\}\}$`);
|
|
730
|
+
/**
|
|
731
|
+
* The problems of the placeholders in \ce and \pu of one template. A placeholder in \ce is its whole argument,
|
|
732
|
+
* \ce{{{e.gl}}}, drawn from a finite list (oneOf, or pick: 1 from a list or a data file), so that the build can set
|
|
733
|
+
* the formula of every value; \pu takes none.
|
|
734
|
+
*/
|
|
735
|
+
function chemProblems(text, given) {
|
|
736
|
+
const problems = [];
|
|
737
|
+
for (const math of formulasIn(text)) for (const { command, argument } of mhchemArguments(math)) {
|
|
738
|
+
if (placeholdersIn(argument).length === 0) continue;
|
|
739
|
+
if (command === "\\pu") problems.push(`\\pu{${argument}} has a placeholder inside; \\pu takes none: write the number before it, e.g. {{m}}\\,\\pu{g}`);
|
|
740
|
+
else if (!WHOLE_PLACEHOLDER.test(argument)) problems.push(`\\ce{${argument}} has a placeholder among other text; in \\ce a placeholder is the whole argument, \\ce{{{e.gl}}}: put the whole formula into one field of the data (gl: "2H2 + O2 -> 2H2O")`);
|
|
741
|
+
}
|
|
742
|
+
for (const path of chemPlaceholdersIn(text)) {
|
|
743
|
+
const root = path.split(".")[0] ?? "";
|
|
744
|
+
if (root === "answer") {
|
|
745
|
+
problems.push("\\ce{{{answer}}} is not possible: the build sets only the formulas of values drawn from a list; show the formula of a value of given");
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
const arbitrary = given[root];
|
|
749
|
+
if (arbitrary === void 0) continue;
|
|
750
|
+
if (arbitrary.oneOf === void 0 && !(arbitrary.from !== void 0 && arbitrary.pick === 1)) problems.push(`\\ce{{{${path}}}} needs one value from a list: draw ${root} with oneOf, or with pick: 1 from a list or a data file, so that the build can set the formula of each value`);
|
|
751
|
+
}
|
|
752
|
+
return problems;
|
|
589
753
|
}
|
|
590
754
|
/** The placeholders of a template, by their path (G, e.name, answer). */
|
|
591
755
|
function placeholdersIn(text) {
|
|
@@ -647,7 +811,7 @@ function writtenTemplates(spec) {
|
|
|
647
811
|
function templateProblems(spec) {
|
|
648
812
|
const declared = [...Object.keys(spec.given), "answer"];
|
|
649
813
|
return writtenTemplates(spec).flatMap(({ path, lang, text }) => {
|
|
650
|
-
return placeholderProblems(text, new Set(declared)).map((message) => ({
|
|
814
|
+
return [...placeholderProblems(text, new Set(declared)), ...chemProblems(text, spec.given)].map((message) => ({
|
|
651
815
|
path,
|
|
652
816
|
...lang === void 0 ? {} : { lang },
|
|
653
817
|
message
|
|
@@ -690,15 +854,23 @@ function placeholderValue(spec, task, path, lang) {
|
|
|
690
854
|
}
|
|
691
855
|
}
|
|
692
856
|
/** A template with the values of a task: HTML (escaped, numbers in formulas as <mn>) or plain text. */
|
|
693
|
-
function fill(template, spec, task, lang, mode = "html") {
|
|
694
|
-
return (mode === "html" ? template.replace(/<mtext>⟦([^⟧]+)⟧<\/mtext>/g, (_whole,
|
|
857
|
+
function fill(template, spec, task, lang, mode = "html", chem = {}) {
|
|
858
|
+
return (mode === "html" ? template.replace(/<mtext>⟦([^⟧]+)⟧<\/mtext>/g, (_whole, token) => {
|
|
859
|
+
if (token.startsWith(CHEM_TOKEN)) {
|
|
860
|
+
const path = token.slice(3);
|
|
861
|
+
const value = placeholderValue(spec, task, path, lang);
|
|
862
|
+
if (value === void 0) return `<mtext>{{${escape(path)}}}</mtext>`;
|
|
863
|
+
return chem[value.text] ?? `<mtext>${escape(value.text)}</mtext>`;
|
|
864
|
+
}
|
|
865
|
+
const path = token;
|
|
695
866
|
const value = placeholderValue(spec, task, path, lang);
|
|
696
867
|
if (value === void 0) return `<mtext>{{${escape(path)}}}</mtext>`;
|
|
697
868
|
if (value.number === void 0) return `<mtext>${escape(value.text)}</mtext>`;
|
|
698
869
|
const negative = value.number.compare(Fraction.of(0)) < 0;
|
|
699
870
|
const digits = escape(negative ? value.text.replace(/^[-−]/, "") : value.text);
|
|
700
871
|
return negative ? `<mo>−</mo><mn>${digits}</mn>` : `<mn>${digits}</mn>`;
|
|
701
|
-
}) : template).replace(/⟦([^⟧]+)⟧/g, (_whole,
|
|
872
|
+
}) : template).replace(/⟦([^⟧]+)⟧/g, (_whole, token) => {
|
|
873
|
+
const path = token.startsWith(CHEM_TOKEN) ? token.slice(3) : token;
|
|
702
874
|
const value = placeholderValue(spec, task, path, lang);
|
|
703
875
|
if (value === void 0) return `{{${path}}}`;
|
|
704
876
|
return mode === "html" ? escape(value.text) : value.text;
|
|
@@ -711,16 +883,18 @@ const templatesIn = (templates, lang) => templates[lang] ?? templates["de"] ?? {
|
|
|
711
883
|
};
|
|
712
884
|
/** The task's text as HTML, in `lang`. */
|
|
713
885
|
function taskText(spec, task, lang, templates) {
|
|
714
|
-
|
|
886
|
+
const { task: text, chem } = templatesIn(templates, lang);
|
|
887
|
+
return fill(text, spec, task, lang, "html", chem);
|
|
715
888
|
}
|
|
716
889
|
/** The steps of the solution as HTML, in `lang`. */
|
|
717
890
|
function stepsOf(spec, task, lang, templates) {
|
|
718
|
-
|
|
891
|
+
const { steps, chem } = templatesIn(templates, lang);
|
|
892
|
+
return steps.map((step) => fill(step, spec, task, lang, "html", chem));
|
|
719
893
|
}
|
|
720
894
|
/** The hint as HTML, in `lang`, if the exercise has one. */
|
|
721
895
|
function hintOf(spec, task, lang, templates) {
|
|
722
|
-
const hint = templatesIn(templates, lang)
|
|
723
|
-
return hint === void 0 ? void 0 : fill(hint, spec, task, lang);
|
|
896
|
+
const { hint, chem } = templatesIn(templates, lang);
|
|
897
|
+
return hint === void 0 ? void 0 : fill(hint, spec, task, lang, "html", chem);
|
|
724
898
|
}
|
|
725
899
|
/** The text of HTML, as a learner reads it: tags left out, entities resolved, white space folded. */
|
|
726
900
|
function plainText(html) {
|
|
@@ -1153,7 +1327,11 @@ const MESSAGES = {
|
|
|
1153
1327
|
openTaskLocked: "Die Lösung öffnet sich nach deiner Antwort.",
|
|
1154
1328
|
resultPartly: "teilweise richtig ({n} von {total} Punkten, es fehlen {missed})",
|
|
1155
1329
|
resultPhoto: "Antwort als Foto",
|
|
1156
|
-
resultAnswer: "Antwort „{input}“"
|
|
1330
|
+
resultAnswer: "Antwort „{input}“",
|
|
1331
|
+
recap: "Wiederholung",
|
|
1332
|
+
recapText: "Diese Seite wiederholt etwas, das du schon können solltest. Üben kannst du es in anderen Apps: {link}",
|
|
1333
|
+
recapLink: "Baustein auf der Lernlandkarte von lernapps.net",
|
|
1334
|
+
recapNote: "Wiederholung"
|
|
1157
1335
|
},
|
|
1158
1336
|
en: {
|
|
1159
1337
|
language: "English",
|
|
@@ -1353,7 +1531,11 @@ const MESSAGES = {
|
|
|
1353
1531
|
openTaskLocked: "The solution opens after your answer.",
|
|
1354
1532
|
resultPartly: "partly right ({n} of {total} points, missing {missed})",
|
|
1355
1533
|
resultPhoto: "answer as a photo",
|
|
1356
|
-
resultAnswer: "answer “{input}”"
|
|
1534
|
+
resultAnswer: "answer “{input}”",
|
|
1535
|
+
recap: "Recap",
|
|
1536
|
+
recapText: "This page goes over something you should already know. You can practise it in other apps: {link}",
|
|
1537
|
+
recapLink: "building block on the learning map of lernapps.net",
|
|
1538
|
+
recapNote: "recap"
|
|
1357
1539
|
}
|
|
1358
1540
|
};
|
|
1359
1541
|
/** The text of `key` in `lang`, with its placeholders filled. */
|
|
@@ -1461,6 +1643,11 @@ function* allBlocks(blocks) {
|
|
|
1461
1643
|
if (block.type === "section" || block.type === "example" || block.type === "note") yield* allBlocks(block.blocks);
|
|
1462
1644
|
}
|
|
1463
1645
|
}
|
|
1646
|
+
/** The page of a competency on the learning map of lernapps.net, from <subject>/<competency> (a recap page's recap). */
|
|
1647
|
+
function mapUrl(recap) {
|
|
1648
|
+
const [subject = "", competency = ""] = recap.split("/");
|
|
1649
|
+
return `https://lernapps.net/apps/map/${subject}/c/${competency}/`;
|
|
1650
|
+
}
|
|
1464
1651
|
/** Whether a block is an exercise. */
|
|
1465
1652
|
const isExercise = (block) => block.type === "generated" || block.type === "open-task" || [
|
|
1466
1653
|
"single-choice",
|
|
@@ -1532,9 +1719,10 @@ function assessments(site) {
|
|
|
1532
1719
|
};
|
|
1533
1720
|
const steps = site.chapters.flatMap((chapter) => chapter.selfAssessment?.pages ?? []);
|
|
1534
1721
|
if (steps.length === 0) return [];
|
|
1535
|
-
const
|
|
1722
|
+
const asked = (pages) => pages.filter((page) => page.recap === void 0);
|
|
1723
|
+
const all = asked(site.chapters.flatMap((chapter) => chapter.pages));
|
|
1536
1724
|
const own = site.chapters.length === 1 ? [] : site.chapters;
|
|
1537
|
-
return [of({ pages: steps }, all), ...own.flatMap((chapter) => chapter.selfAssessment === void 0 ? [] : [of(chapter.selfAssessment, chapter.pages, chapter)])];
|
|
1725
|
+
return [of({ pages: steps }, all), ...own.flatMap((chapter) => chapter.selfAssessment === void 0 ? [] : [of(chapter.selfAssessment, asked(chapter.pages), chapter)])];
|
|
1538
1726
|
}
|
|
1539
1727
|
/** The self-assessment of the app (no chapter) or of the chapter `chapter`, if it has one. */
|
|
1540
1728
|
const assessmentOf = (site, chapter) => assessments(site).find((assessment) => assessment.chapter?.id === chapter);
|
|
@@ -1693,4 +1881,4 @@ function linkLines(root, chapters, whole, lang) {
|
|
|
1693
1881
|
}
|
|
1694
1882
|
/** The title of a content page in `lang`, on one line. */
|
|
1695
1883
|
const titleOf = (page, lang) => oneLine(inLanguage(page.title, lang));
|
|
1696
|
-
export {
|
|
1884
|
+
export { writtenTemplates as $, items as A, chemPlaceholdersIn as B, videoLine as C, compiled as D, StarvedError as E, mistakeOf as F, placeholdersIn as G, hintOf as H, pairValues as I, taskText as J, plainText as K, questionFor as L, answerFor as M, diagnose as N, generate as O, exampleGiving as P, unusedVariables as Q, taskKey as R, startVideos as S, message as T, label as U, fill as V, placeholderValue as W, templatesIn as X, templateProblems as Y, tokenize as Z, mapUrl as _, assessments as a, variables as at, embedAddress as b, isStoredResult as c, parseRef as d, kindOf as et, resultQuery as f, isExercise as g, allBlocks as h, assessmentPrompt as i, same as it, values as j, holds as k, linkLines as l, titleOf as m, BAND_MESSAGES as n, evaluate as nt, drawsTasks as o, Fraction as ot, stepHint as p, stepsOf as q, assessmentOf as r, parse as rt, exerciseOf as s, ASSESSMENT as t, ExpressionError as tt, pageResults as u, termId as v, watchAddress as w, renderVideo as x, timelineEvents as y, answerLabel as z };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { i as inLanguage, l as textsIn, n as escapeText, s as localeOf, t as escape } from "./html-mffkHGHh.mjs";
|
|
2
2
|
import { n as markerOf } from "./block-BK_ohsH1.mjs";
|
|
3
|
-
import { c as renderMarkdown, n as isUrl, s as renderInline, u as renderFormula } from "./check-
|
|
4
|
-
import "./validate-
|
|
3
|
+
import { c as renderMarkdown, n as isUrl, s as renderInline, u as renderFormula } from "./check-BxZOZPic.mjs";
|
|
4
|
+
import "./validate-CfFykN0M.mjs";
|
|
5
5
|
import hljs from "highlight.js/lib/core";
|
|
6
6
|
import bash from "highlight.js/lib/languages/bash";
|
|
7
7
|
import c from "highlight.js/lib/languages/c";
|
package/dist/blocks.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { i as materialIn, n as markerOf, r as materialImages, t as MATERIAL_TYPES } from "./block-BK_ohsH1.mjs";
|
|
2
|
-
import { a as highlight, i as hundredGrid, n as renderMaterial, r as bar, t as renderCredit } from "./blocks-
|
|
3
|
-
import { n as checkMaterial, t as MATERIAL_DEFINITIONS } from "./validate-
|
|
2
|
+
import { a as highlight, i as hundredGrid, n as renderMaterial, r as bar, t as renderCredit } from "./blocks-D4SSGKuL.mjs";
|
|
3
|
+
import { n as checkMaterial, t as MATERIAL_DEFINITIONS } from "./validate-CfFykN0M.mjs";
|
|
4
4
|
export { MATERIAL_DEFINITIONS, MATERIAL_TYPES, bar, checkMaterial, highlight, hundredGrid, markerOf, materialImages, materialIn, renderCredit, renderMaterial };
|
|
@@ -4,7 +4,8 @@ import { Marked } from "marked";
|
|
|
4
4
|
// runtime/shared/text/formula.ts
|
|
5
5
|
globalThis.temml = temml;
|
|
6
6
|
await import("temml/contrib/mhchem/mhchem.js");
|
|
7
|
-
|
|
7
|
+
/** The commands a formula may use (explainer.v1.schema.json, $defs/latex), besides ^, _, braces and parentheses. */
|
|
8
|
+
const COMMANDS = [
|
|
8
9
|
"\\frac",
|
|
9
10
|
"\\cdot",
|
|
10
11
|
"\\times",
|
|
@@ -21,8 +22,61 @@ const allowed = /* @__PURE__ */ new Set([
|
|
|
21
22
|
"\\{",
|
|
22
23
|
"\\}",
|
|
23
24
|
"\\ce",
|
|
24
|
-
"\\pu"
|
|
25
|
-
|
|
25
|
+
"\\pu",
|
|
26
|
+
"\\ ",
|
|
27
|
+
"\\quad",
|
|
28
|
+
"\\qquad",
|
|
29
|
+
"\\Rightarrow",
|
|
30
|
+
"\\Leftrightarrow",
|
|
31
|
+
"\\left",
|
|
32
|
+
"\\right",
|
|
33
|
+
"\\sin",
|
|
34
|
+
"\\cos",
|
|
35
|
+
"\\tan",
|
|
36
|
+
"\\arcsin",
|
|
37
|
+
"\\arccos",
|
|
38
|
+
"\\arctan",
|
|
39
|
+
"\\circ",
|
|
40
|
+
...[
|
|
41
|
+
"alpha",
|
|
42
|
+
"beta",
|
|
43
|
+
"gamma",
|
|
44
|
+
"delta",
|
|
45
|
+
"varepsilon",
|
|
46
|
+
"epsilon",
|
|
47
|
+
"zeta",
|
|
48
|
+
"eta",
|
|
49
|
+
"theta",
|
|
50
|
+
"vartheta",
|
|
51
|
+
"iota",
|
|
52
|
+
"kappa",
|
|
53
|
+
"lambda",
|
|
54
|
+
"mu",
|
|
55
|
+
"nu",
|
|
56
|
+
"xi",
|
|
57
|
+
"pi",
|
|
58
|
+
"rho",
|
|
59
|
+
"sigma",
|
|
60
|
+
"tau",
|
|
61
|
+
"upsilon",
|
|
62
|
+
"varphi",
|
|
63
|
+
"phi",
|
|
64
|
+
"chi",
|
|
65
|
+
"psi",
|
|
66
|
+
"omega",
|
|
67
|
+
"Gamma",
|
|
68
|
+
"Delta",
|
|
69
|
+
"Theta",
|
|
70
|
+
"Lambda",
|
|
71
|
+
"Xi",
|
|
72
|
+
"Pi",
|
|
73
|
+
"Sigma",
|
|
74
|
+
"Phi",
|
|
75
|
+
"Psi",
|
|
76
|
+
"Omega"
|
|
77
|
+
].map((letter) => `\\${letter}`)
|
|
78
|
+
];
|
|
79
|
+
const allowed = new Set(COMMANDS);
|
|
26
80
|
/** The commands of `tex` outside the arguments of \ce and \pu, which mhchem reads in its own notation. */
|
|
27
81
|
function commands(tex) {
|
|
28
82
|
const found = [];
|
|
@@ -42,7 +96,7 @@ function commands(tex) {
|
|
|
42
96
|
/** What is wrong with the formula `tex`, or undefined when it is in the command list and parses. */
|
|
43
97
|
function formulaProblem(tex) {
|
|
44
98
|
const unknown = commands(tex).find((command) => !allowed.has(command));
|
|
45
|
-
if (unknown !== void 0) return `${unknown} is not in the formula command list;
|
|
99
|
+
if (unknown !== void 0) return `${unknown} is not in the formula command list; use only these commands, besides ^, _, braces and parentheses: ${COMMANDS.map((command) => command === "\\ " ? "\\ (a space)" : command).join(", ")}`;
|
|
46
100
|
try {
|
|
47
101
|
temml.renderToString(tex, { throwOnError: true });
|
|
48
102
|
return;
|
package/dist/explainer-e2e.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { a as routes, d as templatesOf, f as workedTasks, l as renderExercise, n as loadSite, s as dataOf, u as taskSeeds } from "./load
|
|
1
|
+
import { a as routes, d as templatesOf, f as workedTasks, l as renderExercise, n as loadSite, s as dataOf, u as taskSeeds } from "./load--ovXqbNE.mjs";
|
|
2
2
|
import { i as inLanguage } from "./html-mffkHGHh.mjs";
|
|
3
|
-
import { a as inspect, c as renderMarkdown, l as renderWithFormulas } from "./check-
|
|
3
|
+
import { a as inspect, c as renderMarkdown, l as renderWithFormulas } from "./check-BxZOZPic.mjs";
|
|
4
4
|
import { C as wrongAnswer, _ as questionIn, b as rightAnswer, f as missesRight, g as questionId, i as questionMessagesIn, n as answerText, w as wrongReasons } from "./render-GS1Htlu8.mjs";
|
|
5
5
|
import { a as giveAnswer, i as expectMaterial, r as expectFeedbackInView, t as answerQuestion } from "./e2e-CU0n_unV.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import { n as answerShown } from "./result-
|
|
6
|
+
import { J as taskText, K as plainText, L as questionFor, M as answerFor, N as diagnose, O as generate, R as taskKey, T as message, V as fill, a as assessments, b as embedAddress, et as kindOf, m as titleOf, w as watchAddress, y as timelineEvents } from "./assessment-DaKsXUdj.mjs";
|
|
7
|
+
import { n as answerShown } from "./result-DphIWHDF.mjs";
|
|
8
8
|
import { n as ASSESSMENT_TASK_EVENT, p as taskNumber, u as nextTaskNumber } from "./tasks-DADUYy52.mjs";
|
|
9
9
|
import { expect, test } from "playwright/test";
|
|
10
10
|
// runtime/explainer/outline.ts
|
|
@@ -94,7 +94,7 @@ function walk(site, blocks, lang, level, anchor, into) {
|
|
|
94
94
|
}
|
|
95
95
|
case "generated": {
|
|
96
96
|
const data = dataOf(block, site.data);
|
|
97
|
-
const templates = templatesOf(block, [lang]);
|
|
97
|
+
const templates = templatesOf(block, [lang], data);
|
|
98
98
|
const worked = workedTasks(block, data, templates, lang);
|
|
99
99
|
for (const item of worked) for (const html of [
|
|
100
100
|
item.task,
|