@lernapps/app-templates 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{assessment-By2GGTJC.mjs → assessment-Dy359O1O.mjs} +1 -1
- package/dist/{blocks-BmRrD7UY.mjs → blocks-CxtCPAa5.mjs} +2 -2
- package/dist/blocks.mjs +2 -2
- package/dist/{check-BkEl-m-F.mjs → check-CteIFN96.mjs} +1 -1
- package/dist/{content-cYBPYg_J.mjs → content-DO6oEJxj.mjs} +1 -1
- package/dist/{e2e-BiMI-pCL.mjs → e2e-CU0n_unV.mjs} +1 -1
- package/dist/{engine-BqF2Vffn.mjs → engine-CArxqizW.mjs} +1 -1
- package/dist/explainer-e2e.mjs +27 -13
- package/dist/explainer-plugin.mjs +6 -5
- package/dist/explainer.mjs +4 -4
- package/dist/interactive-e2e.mjs +1 -1
- package/dist/interactive-plugin.mjs +2 -2
- package/dist/{load-C-0hUaSC.mjs → load-RosvaPZf.mjs} +37 -8
- package/dist/questions-e2e.mjs +1 -1
- package/dist/questions-engine.mjs +2 -2
- package/dist/questions.mjs +2 -2
- package/dist/{quiz-BKc8JYtC.mjs → quiz-DnW-xtCh.mjs} +1 -1
- package/dist/quiz-e2e.mjs +4 -4
- package/dist/quiz-plugin.mjs +4 -4
- package/dist/quiz.mjs +3 -3
- package/dist/{render-BL6fV_NI.mjs → render-D5kwkNt1.mjs} +2 -2
- package/dist/{render-DpRO_RKm.mjs → render-DqHOJyHK.mjs} +3 -3
- package/dist/{render-BJey0bwl.mjs → render-GS1Htlu8.mjs} +9 -4
- package/dist/{result-_eQcamoz.mjs → result-D_mmCVwN.mjs} +2 -2
- package/dist/{validate-COaZso7u.mjs → validate-BDwL7Su3.mjs} +6 -2
- package/dist/{validate-CxtdeQ5S.mjs → validate-Bxy9NfB8.mjs} +1 -1
- package/explainer/package.json +1 -1
- package/interactive/package.json +1 -1
- package/package.json +2 -1
- package/quiz/package.json +1 -1
- package/runtime/explainer/assessment-page.ts +2 -0
- package/runtime/explainer/e2e.ts +39 -25
- package/runtime/explainer/exercises.ts +2 -0
- package/runtime/explainer/outline.ts +8 -1
- package/runtime/shared/questions/question.ts +3 -0
- package/runtime/shared/questions/question.v1.schema.json +4 -0
- package/runtime/shared/questions/render.ts +14 -6
- package/runtime/shared/text/markdown.ts +1 -1
- package/runtime/shared/text/speech.ts +39 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as inLanguage, l as textsIn, s as localeOf, t as escape } from "./html-mffkHGHh.mjs";
|
|
2
|
-
import { S as shuffled, c as hash, m as parseNumber, u as isCorrect, y as random } from "./render-
|
|
2
|
+
import { S as shuffled, c as hash, m as parseNumber, u as isCorrect, y as random } from "./render-GS1Htlu8.mjs";
|
|
3
3
|
import { createStorage } from "@lernapps/tooling/storage";
|
|
4
4
|
// runtime/explainer/fraction.ts
|
|
5
5
|
const abs = (n) => n < 0n ? -n : n;
|
|
@@ -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 {
|
|
4
|
-
import "./validate-
|
|
3
|
+
import { c as renderMarkdown, n as isUrl, s as renderInline, u as renderFormula } from "./check-CteIFN96.mjs";
|
|
4
|
+
import "./validate-Bxy9NfB8.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-CxtCPAa5.mjs";
|
|
3
|
+
import { n as checkMaterial, t as MATERIAL_DEFINITIONS } from "./validate-Bxy9NfB8.mjs";
|
|
4
4
|
export { MATERIAL_DEFINITIONS, MATERIAL_TYPES, bar, checkMaterial, highlight, hundredGrid, markerOf, materialImages, materialIn, renderCredit, renderMaterial };
|
|
@@ -251,4 +251,4 @@ function checkText(value, kind, languages, declaredIn) {
|
|
|
251
251
|
links
|
|
252
252
|
};
|
|
253
253
|
}
|
|
254
|
-
export {
|
|
254
|
+
export { inspect as a, renderMarkdown as c, formulaProblems as i, renderWithFormulas as l, isUrl as n, plain as o, FORMULA as r, renderInline as s, checkText as t, renderFormula as u };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { l as textsIn } from "./html-mffkHGHh.mjs";
|
|
2
|
-
import { g as questionId, t as QUESTION_MESSAGES } from "./render-
|
|
2
|
+
import { g as questionId, t as QUESTION_MESSAGES } from "./render-GS1Htlu8.mjs";
|
|
3
3
|
import { expect } from "playwright/test";
|
|
4
4
|
// runtime/shared/questions/e2e.ts
|
|
5
5
|
/** The question's own texts in `lang`. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { l as textsIn } from "./html-mffkHGHh.mjs";
|
|
2
|
-
import { S as shuffled, c as hash, f as missesRight, l as isComplete, t as QUESTION_MESSAGES, u as isCorrect, w as wrongReasons } from "./render-
|
|
2
|
+
import { S as shuffled, c as hash, f as missesRight, l as isComplete, t as QUESTION_MESSAGES, u as isCorrect, w as wrongReasons } from "./render-GS1Htlu8.mjs";
|
|
3
3
|
import { translator } from "@lernapps/tooling/i18n";
|
|
4
4
|
import { moveFocus } from "@lernapps/tooling/a11y";
|
|
5
5
|
// runtime/shared/questions/engine.ts
|
package/dist/explainer-e2e.mjs
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
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-RosvaPZf.mjs";
|
|
2
2
|
import { i as inLanguage } from "./html-mffkHGHh.mjs";
|
|
3
|
-
import {
|
|
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-
|
|
5
|
-
import { a as giveAnswer, i as expectMaterial, r as expectFeedbackInView, t as answerQuestion } from "./e2e-
|
|
6
|
-
import { A as answerFor, C as watchAddress, D as generate, F as questionFor, G as taskText, I as taskKey, R as fill, U as plainText, X as kindOf, a as assessments, j as diagnose, m as titleOf, v as timelineEvents, w as message, y as embedAddress } from "./assessment-
|
|
7
|
-
import { n as answerShown } from "./result-
|
|
3
|
+
import { a as inspect, c as renderMarkdown, l as renderWithFormulas } from "./check-CteIFN96.mjs";
|
|
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
|
+
import { a as giveAnswer, i as expectMaterial, r as expectFeedbackInView, t as answerQuestion } from "./e2e-CU0n_unV.mjs";
|
|
6
|
+
import { A as answerFor, C as watchAddress, D as generate, F as questionFor, G as taskText, I as taskKey, R as fill, U as plainText, X as kindOf, a as assessments, j as diagnose, m as titleOf, v as timelineEvents, w as message, y as embedAddress } from "./assessment-Dy359O1O.mjs";
|
|
7
|
+
import { n as answerShown } from "./result-D_mmCVwN.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
|
|
11
|
+
/**
|
|
12
|
+
* A learner text of a question as the page shows it, as text: its formulas $…$ rendered (a formula has no space the
|
|
13
|
+
* tests could rely on in a browser's text, see squash in e2e.ts), the rest as written.
|
|
14
|
+
*/
|
|
15
|
+
const shownText = (text) => plainText(renderWithFormulas(text));
|
|
11
16
|
/** The headings, sections, formulas and figures of `blocks` in `lang`, under a heading of level `level`. */
|
|
12
17
|
function walk(site, blocks, lang, level, anchor, into) {
|
|
13
18
|
const text = (value) => inLanguage(value, lang);
|
|
@@ -142,7 +147,7 @@ function walk(site, blocks, lang, level, anchor, into) {
|
|
|
142
147
|
id: block.id,
|
|
143
148
|
type: block.type,
|
|
144
149
|
question,
|
|
145
|
-
answers: question.type === "single-choice" || question.type === "multiple-choice" ? question.options.filter((option) => option.correct).map((option) => option.text) : [plainText(answerText(question, lang))]
|
|
150
|
+
answers: question.type === "single-choice" || question.type === "multiple-choice" ? question.options.filter((option) => option.correct).map((option) => shownText(option.text)) : [plainText(answerText(question, lang))]
|
|
146
151
|
});
|
|
147
152
|
}
|
|
148
153
|
}
|
|
@@ -279,6 +284,11 @@ const BAND_WORDS = {
|
|
|
279
284
|
read: "assessmentRead",
|
|
280
285
|
unasked: "assessmentUnasked"
|
|
281
286
|
};
|
|
287
|
+
/**
|
|
288
|
+
* `text` without space. A browser's text of a formula has space between its parts ("2 7" for 2⁷) where the content's
|
|
289
|
+
* has none, so texts with formulas are compared without it (in the order of their characters, which is what counts).
|
|
290
|
+
*/
|
|
291
|
+
const squash = (text) => text.replace(/\s+/g, "");
|
|
282
292
|
/** The task number of the deep link the tests open a generated exercise with. */
|
|
283
293
|
const SEED = 7;
|
|
284
294
|
/**
|
|
@@ -290,7 +300,7 @@ async function expectExercises(page, item) {
|
|
|
290
300
|
const main = page.locator("main");
|
|
291
301
|
const expected = item.exercises.map((exercise) => exercise.kind === "question" ? {
|
|
292
302
|
id: exercise.id,
|
|
293
|
-
texts: [exercise.question.text],
|
|
303
|
+
texts: [shownText(exercise.question.text)],
|
|
294
304
|
answers: [exercise.answers]
|
|
295
305
|
} : exercise.kind === "open" ? {
|
|
296
306
|
id: exercise.id,
|
|
@@ -301,7 +311,7 @@ async function expectExercises(page, item) {
|
|
|
301
311
|
texts: exercise.worked.map((worked) => worked.task),
|
|
302
312
|
answers: exercise.worked.map((worked) => [worked.answer])
|
|
303
313
|
});
|
|
304
|
-
const read = () => main.locator(".explainer-exercise").evaluateAll((elements) => elements.map((element) => {
|
|
314
|
+
const read = async () => (await main.locator(".explainer-exercise").evaluateAll((elements) => elements.map((element) => {
|
|
305
315
|
const fold = (text) => (text ?? "").replace(/\s+/g, " ").trim();
|
|
306
316
|
const own = element.classList.contains("explainer-question") ? [...element.querySelectorAll("legend")] : element.classList.contains("explainer-open-task") ? [...element.querySelectorAll(":scope > .explainer-open-task-text")] : [...element.querySelectorAll(".explainer-worked-task > .explainer-task")];
|
|
307
317
|
const solutions = [...element.querySelectorAll("details.explainer-solution")];
|
|
@@ -310,6 +320,10 @@ async function expectExercises(page, item) {
|
|
|
310
320
|
texts: own.map((text) => fold(text.textContent)),
|
|
311
321
|
shown: solutions.map((details) => details.open ? fold(details.innerText) : "")
|
|
312
322
|
};
|
|
323
|
+
}))).map((item) => ({
|
|
324
|
+
...item,
|
|
325
|
+
texts: item.texts.map(squash),
|
|
326
|
+
shown: item.shown.map(squash)
|
|
313
327
|
}));
|
|
314
328
|
const before = await read();
|
|
315
329
|
expect(before.map(({ id, texts }) => ({
|
|
@@ -317,12 +331,12 @@ async function expectExercises(page, item) {
|
|
|
317
331
|
texts
|
|
318
332
|
})), `${item.url}: each exercise with its text`).toEqual(expected.map(({ id, texts }) => ({
|
|
319
333
|
id,
|
|
320
|
-
texts
|
|
334
|
+
texts: texts.map(squash)
|
|
321
335
|
})));
|
|
322
336
|
await main.locator(".explainer-exercise details.explainer-solution > summary").evaluateAll((summaries) => summaries.forEach((summary) => summary.click()));
|
|
323
337
|
const after = await read();
|
|
324
338
|
for (const [index, exercise] of expected.entries()) exercise.answers.forEach((answers, at) => {
|
|
325
|
-
for (const answer of answers) expect(after[index]?.shown[at] ?? "", `${item.url}: ${exercise.id}, Lösung ${at + 1} shows ${answer}`).toContain(answer);
|
|
339
|
+
for (const answer of answers) expect(after[index]?.shown[at] ?? "", `${item.url}: ${exercise.id}, Lösung ${at + 1} shows ${answer}`).toContain(squash(answer));
|
|
326
340
|
});
|
|
327
341
|
}
|
|
328
342
|
/** Without JavaScript: each component's nojs text is visible, not empty and as in the content; its island is hidden. */
|
|
@@ -535,7 +549,7 @@ const feedback = (page, id) => page.locator(`[id="${id}"] .question-feedback`);
|
|
|
535
549
|
function reasonFor(question, answer) {
|
|
536
550
|
const quiz = questionMessagesIn("de");
|
|
537
551
|
const [first] = wrongReasons(question, answer);
|
|
538
|
-
if (first !== void 0) return first.option === void 0 ? first.text : `${first.option}: ${first.text}`;
|
|
552
|
+
if (first !== void 0) return first.option === void 0 ? shownText(first.text) : `${shownText(first.option)}: ${shownText(first.text)}`;
|
|
539
553
|
return missesRight(question, answer) ? quiz.missingRight : quiz.lookAgain;
|
|
540
554
|
}
|
|
541
555
|
/**
|
|
@@ -545,7 +559,7 @@ function reasonFor(question, answer) {
|
|
|
545
559
|
async function wrongThenSolution(page, exercise, question, reason) {
|
|
546
560
|
const item = page.locator(`[id="${questionId(question)}"]`);
|
|
547
561
|
await expect(feedback(page, questionId(question))).toContainText(questionMessagesIn("de").wrong);
|
|
548
|
-
await expect(item.locator(".question-diagnosis").first(), `${exercise}: the reason of the wrong answer`).
|
|
562
|
+
await expect.poll(async () => squash(await item.locator(".question-diagnosis").first().textContent() ?? ""), `${exercise}: the reason of the wrong answer`).toBe(squash(reason));
|
|
549
563
|
await expectFeedbackInView(page, question);
|
|
550
564
|
await expect(item.locator(".question-reveal"), `${exercise}: no solution after a wrong answer`).toBeHidden();
|
|
551
565
|
await expect(item.locator("input, select").first(), `${exercise}: another try`).toBeEnabled();
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { _ as exampleProblems, a as routes, c as exerciseData, d as templatesOf, g as describedProblems, h as TABLE_CONTRACT, i as hiddenChapter, l as renderExercise, m as SIZE_BUDGET, n as loadSite, o as terms, p as formulaAlternative, r as contentPages, s as dataOf, t as ContentError, u as taskSeeds, v as propertyProblems, y as propsIn } from "./load-RosvaPZf.mjs";
|
|
2
2
|
import { i as inLanguage, s as localeOf, t as escape } from "./html-mffkHGHh.mjs";
|
|
3
3
|
import { n as markerOf } from "./block-BK_ohsH1.mjs";
|
|
4
|
-
import { n as renderMaterial } from "./blocks-
|
|
5
|
-
import {
|
|
6
|
-
import { _ as questionIn, a as renderQuestion, i as questionMessagesIn, n as answerText } from "./render-
|
|
4
|
+
import { n as renderMaterial } from "./blocks-CxtCPAa5.mjs";
|
|
5
|
+
import { c as renderMarkdown, l as renderWithFormulas, o as plain, s as renderInline } from "./check-CteIFN96.mjs";
|
|
6
|
+
import { _ as questionIn, a as renderQuestion, i as questionMessagesIn, n as answerText } from "./render-GS1Htlu8.mjs";
|
|
7
7
|
import { n as isSizeless, t as imageType } from "./image-CspJVfyQ.mjs";
|
|
8
8
|
import { a as pageFooter, c as withLanguage, i as notFoundPage, n as fromDepth, o as pageHeader, r as languageRoot, s as relative, t as alternateLinks } from "./pages-9RaaomoO.mjs";
|
|
9
|
-
import { B as label, D as generate, F as questionFor, H as placeholdersIn, S as videoLine, U as plainText, V as placeholderValue, X as kindOf, _ as termId, a as assessments, b as renderVideo, h as allBlocks, m as titleOf, o as drawsTasks, p as stepHint, r as assessmentOf, v as timelineEvents, w as message } from "./assessment-
|
|
9
|
+
import { B as label, D as generate, F as questionFor, H as placeholdersIn, S as videoLine, U as plainText, V as placeholderValue, X as kindOf, _ as termId, a as assessments, b as renderVideo, h as allBlocks, m as titleOf, o as drawsTasks, p as stepHint, r as assessmentOf, v as timelineEvents, w as message } from "./assessment-Dy359O1O.mjs";
|
|
10
10
|
import { i as NEW_TASK_ICON } from "./tasks-DADUYy52.mjs";
|
|
11
11
|
import { n as NEEDS, o as languageName, t as LANGUAGES } from "./needs-BzEahJ6X.mjs";
|
|
12
12
|
import { n as vueAlias, r as withServer, t as VUE_OPTIONS } from "./vue-DO1m6q1R.mjs";
|
|
@@ -909,6 +909,7 @@ function itemHtml(context, data, n, component = "") {
|
|
|
909
909
|
return `${label}${renderQuestion(question, {
|
|
910
910
|
lang,
|
|
911
911
|
format: (text) => rendered[text] ?? renderWithFormulas(text),
|
|
912
|
+
describe: (text) => formulaAlternative(text, lang),
|
|
912
913
|
...question.material === void 0 ? {} : { materialHtml: renderMaterial(question.material, context.material) }
|
|
913
914
|
})}`;
|
|
914
915
|
}
|
package/dist/explainer.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as escape } from "./html-mffkHGHh.mjs";
|
|
2
|
-
import { S as shuffled, a as renderQuestion, g as questionId, l as isComplete, u as isCorrect } from "./render-
|
|
3
|
-
import { n as startQuestion, t as readAnswer } from "./engine-
|
|
4
|
-
import { D as generate, F as questionFor, G as taskText, N as mistakeOf, W as stepsOf, c as isStoredResult, f as resultQuery, i as assessmentPrompt, j as diagnose, l as linkLines, n as BAND_MESSAGES, u as pageResults, w as message, x as startVideos, z as hintOf } from "./assessment-
|
|
5
|
-
import { i as sessionLines, n as answerShown, r as resultLine, t as Progress } from "./result-
|
|
2
|
+
import { S as shuffled, a as renderQuestion, g as questionId, l as isComplete, u as isCorrect } from "./render-GS1Htlu8.mjs";
|
|
3
|
+
import { n as startQuestion, t as readAnswer } from "./engine-CArxqizW.mjs";
|
|
4
|
+
import { D as generate, F as questionFor, G as taskText, N as mistakeOf, W as stepsOf, c as isStoredResult, f as resultQuery, i as assessmentPrompt, j as diagnose, l as linkLines, n as BAND_MESSAGES, u as pageResults, w as message, x as startVideos, z as hintOf } from "./assessment-Dy359O1O.mjs";
|
|
5
|
+
import { i as sessionLines, n as answerShown, r as resultLine, t as Progress } from "./result-D_mmCVwN.mjs";
|
|
6
6
|
import { c as addressOf, d as numberFromAddress, f as randomTaskNumber, i as NEW_TASK_ICON, n as ASSESSMENT_TASK_EVENT, p as taskNumber, r as ASSESSMENT_UNPACKED_EVENT, s as SHOWN_EVENT, t as ASSESSMENT_ANSWER_EVENT, u as nextTaskNumber } from "./tasks-DADUYy52.mjs";
|
|
7
7
|
import { a as isNeedsChoice, c as withNeeds, r as NEEDS_KEY, s as withLanguage } from "./needs-BzEahJ6X.mjs";
|
|
8
8
|
import { moveFocus } from "@lernapps/tooling/a11y";
|
package/dist/interactive-e2e.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { i as inLanguage } from "./html-mffkHGHh.mjs";
|
|
2
2
|
import { r as languageRoot } from "./pages-9RaaomoO.mjs";
|
|
3
3
|
import { i as doingText, n as STATE_PROPERTY, o as inputsOf, s as mismatch, t as CLOCK_PROPERTY } from "./contract-PONpxJtd.mjs";
|
|
4
|
-
import { i as message, o as loadInteractive, t as APP_PAGE } from "./render-
|
|
4
|
+
import { i as message, o as loadInteractive, t as APP_PAGE } from "./render-D5kwkNt1.mjs";
|
|
5
5
|
import { expect, test } from "playwright/test";
|
|
6
6
|
// runtime/interactive/e2e.ts
|
|
7
7
|
/** The page's language, its alternates and a switch to each other language, the archetype marker. */
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { c as withLanguage, i as notFoundPage, n as fromDepth } from "./pages-9RaaomoO.mjs";
|
|
2
2
|
import { c as sourceScope, n as codeProblems } from "./vue-code-BkJjW_I_.mjs";
|
|
3
|
-
import { o as pathText, t as Problems } from "./content-
|
|
3
|
+
import { o as pathText, t as Problems } from "./content-DO6oEJxj.mjs";
|
|
4
4
|
import { n as vueAlias, r as withServer, t as VUE_OPTIONS } from "./vue-DO1m6q1R.mjs";
|
|
5
5
|
import { a as inputText, i as doingText, o as inputsOf, s as mismatch } from "./contract-PONpxJtd.mjs";
|
|
6
|
-
import { a as InteractiveError, n as pages, o as loadInteractive, r as renderPage } from "./render-
|
|
6
|
+
import { a as InteractiveError, n as pages, o as loadInteractive, r as renderPage } from "./render-D5kwkNt1.mjs";
|
|
7
7
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import vue from "@vitejs/plugin-vue";
|
|
@@ -1,19 +1,20 @@
|
|
|
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-
|
|
3
|
-
import { a as
|
|
4
|
-
import { n as checkMaterial } from "./validate-
|
|
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-
|
|
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-
|
|
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";
|
|
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";
|
|
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-
|
|
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";
|
|
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-
|
|
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";
|
|
13
13
|
import "node:module";
|
|
14
14
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
15
15
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
+
import untyped from "speech-rule-engine";
|
|
17
18
|
// \0rolldown/runtime.js
|
|
18
19
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod, mod.exports), cb = null), mod.exports);
|
|
19
20
|
// runtime/explainer/components/props.ts
|
|
@@ -14709,6 +14710,33 @@ function readComponents(root) {
|
|
|
14709
14710
|
};
|
|
14710
14711
|
}
|
|
14711
14712
|
/** A problem with a block's props, at a path below props. */
|
|
14713
|
+
// runtime/shared/text/speech.ts
|
|
14714
|
+
const SRE = untyped;
|
|
14715
|
+
const SPEECH = {
|
|
14716
|
+
domain: "clearspeak",
|
|
14717
|
+
modality: "speech"
|
|
14718
|
+
};
|
|
14719
|
+
for (const locale of LANGUAGES) await SRE.setupEngine({
|
|
14720
|
+
locale,
|
|
14721
|
+
...SPEECH
|
|
14722
|
+
});
|
|
14723
|
+
/** The formula `tex` as words in `lang` (German for a language the runtime has no texts for), e.g. "2 hoch 7". */
|
|
14724
|
+
function speakFormula(tex, lang) {
|
|
14725
|
+
SRE.setupEngine({
|
|
14726
|
+
locale: isLanguage(lang) ? lang : "de",
|
|
14727
|
+
...SPEECH
|
|
14728
|
+
});
|
|
14729
|
+
return SRE.toSpeech(renderFormula(tex, false)).replace(/\s+/g, " ").trim();
|
|
14730
|
+
}
|
|
14731
|
+
/**
|
|
14732
|
+
* The words of a text that is only formulas, $…$ (and space); undefined for any other text, which has words of its own
|
|
14733
|
+
* for a name to be made of.
|
|
14734
|
+
*/
|
|
14735
|
+
function formulaAlternative(text, lang) {
|
|
14736
|
+
const formulas = [...text.matchAll(FORMULA)];
|
|
14737
|
+
if (formulas.length === 0 || text.replace(FORMULA, "").trim() !== "") return void 0;
|
|
14738
|
+
return formulas.map((formula) => speakFormula(formula[1] ?? "", lang)).join(" ");
|
|
14739
|
+
}
|
|
14712
14740
|
// runtime/explainer/exercises.ts
|
|
14713
14741
|
/** The templates of a generated exercise in each of `languages`; links inside them through `resolve`. */
|
|
14714
14742
|
function templatesOf(spec, languages, resolve = (href) => href) {
|
|
@@ -14795,6 +14823,7 @@ function questionExercise(block, lang, material) {
|
|
|
14795
14823
|
renderQuestion(question, {
|
|
14796
14824
|
lang,
|
|
14797
14825
|
format: renderWithFormulas,
|
|
14826
|
+
describe: (text) => formulaAlternative(text, lang),
|
|
14798
14827
|
...question.material === void 0 ? {} : { materialHtml: renderMaterial(question.material, material) }
|
|
14799
14828
|
}),
|
|
14800
14829
|
`<details class="explainer-solution"><summary>${plain(questionMessagesIn(lang).solution)}</summary>`,
|
|
@@ -16944,4 +16973,4 @@ function loadSite(root, seeds = 200) {
|
|
|
16944
16973
|
if (site === void 0) throw new ContentError(problems);
|
|
16945
16974
|
return site;
|
|
16946
16975
|
}
|
|
16947
|
-
export {
|
|
16976
|
+
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 };
|
package/dist/questions-e2e.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as answerQuestion, n as expectBackground, r as expectFeedbackInView, i as expectMaterial, a as giveAnswer, o as question } from "./e2e-
|
|
1
|
+
export { t as answerQuestion, n as expectBackground, r as expectFeedbackInView, i as expectMaterial, a as giveAnswer, o as question } from "./e2e-CU0n_unV.mjs";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { g as questionId } from "./render-
|
|
2
|
-
import { n as startQuestion, t as readAnswer } from "./engine-
|
|
1
|
+
import { g as questionId } from "./render-GS1Htlu8.mjs";
|
|
2
|
+
import { n as startQuestion, t as readAnswer } from "./engine-CArxqizW.mjs";
|
|
3
3
|
export { questionId, readAnswer, startQuestion };
|
package/dist/questions.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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-
|
|
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-
|
|
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";
|
|
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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as inLanguage } from "./html-mffkHGHh.mjs";
|
|
2
|
-
import { _ as questionIn } from "./render-
|
|
2
|
+
import { _ as questionIn } from "./render-GS1Htlu8.mjs";
|
|
3
3
|
// runtime/quiz/messages/de.json
|
|
4
4
|
var de_default = {
|
|
5
5
|
howto: "Beantworte die Fragen der Reihe nach. Nach jeder Antwort siehst du, ob sie stimmt, und erfährst mehr über jede Antwort. Am Ende siehst du dein Ergebnis.",
|
package/dist/quiz-e2e.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { C as wrongAnswer, b as rightAnswer, g as questionId } from "./render-
|
|
2
|
-
import { i as expectMaterial, n as expectBackground, o as question, r as expectFeedbackInView, t as answerQuestion } from "./e2e-
|
|
3
|
-
import { n as quizLanguages, t as quizIn } from "./quiz-
|
|
4
|
-
import { n as messagesIn, t as QUIZ_MESSAGES } from "./render-
|
|
1
|
+
import { C as wrongAnswer, b as rightAnswer, g as questionId } from "./render-GS1Htlu8.mjs";
|
|
2
|
+
import { i as expectMaterial, n as expectBackground, o as question, r as expectFeedbackInView, t as answerQuestion } from "./e2e-CU0n_unV.mjs";
|
|
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";
|
|
5
5
|
import { expect, test } from "playwright/test";
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
7
|
// runtime/quiz/e2e.ts
|
package/dist/quiz-plugin.mjs
CHANGED
|
@@ -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-
|
|
4
|
-
import { a as duplicates, d as readable, f as schemaMessages, o as inQuestion, p as validateQuestion, u as questionSchemas } from "./validate-
|
|
5
|
-
import { n as quizLanguages, t as quizIn } from "./quiz-
|
|
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";
|
|
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-
|
|
9
|
+
import { r as renderQuiz } from "./render-DqHOJyHK.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";
|
package/dist/quiz.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { S as shuffled, g as questionId, u as isCorrect } from "./render-
|
|
2
|
-
import { n as startQuestion } from "./engine-
|
|
3
|
-
import { a as de_default, i as en_default, r as solutionId } from "./quiz-
|
|
1
|
+
import { S as shuffled, g as questionId, u as isCorrect } from "./render-GS1Htlu8.mjs";
|
|
2
|
+
import { n as startQuestion } from "./engine-CArxqizW.mjs";
|
|
3
|
+
import { a as de_default, i as en_default, r as solutionId } from "./quiz-DnW-xtCh.mjs";
|
|
4
4
|
import { translator } from "@lernapps/tooling/i18n";
|
|
5
5
|
import { announce, moveFocus } from "@lernapps/tooling/a11y";
|
|
6
6
|
import { createStorage } from "@lernapps/tooling/storage";
|
|
@@ -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 {
|
|
2
|
+
import { c as renderMarkdown, o as plain } from "./check-CteIFN96.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-
|
|
4
|
+
import { c as schemaProblems, n as Texts, s as readYaml, t as Problems } from "./content-DO6oEJxj.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,7 +1,7 @@
|
|
|
1
1
|
import { l as textsIn, s as localeOf, t as escape } from "./html-mffkHGHh.mjs";
|
|
2
|
-
import { n as renderMaterial } from "./blocks-
|
|
3
|
-
import { a as renderQuestion, g as questionId, o as renderSolution, r as external, t as QUESTION_MESSAGES } from "./render-
|
|
4
|
-
import { a as de_default, i as en_default, r as solutionId } from "./quiz-
|
|
2
|
+
import { n as renderMaterial } from "./blocks-CxtCPAa5.mjs";
|
|
3
|
+
import { a as renderQuestion, g as questionId, o as renderSolution, r as external, t as QUESTION_MESSAGES } from "./render-GS1Htlu8.mjs";
|
|
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";
|
|
6
6
|
import { translator } from "@lernapps/tooling/i18n";
|
|
7
7
|
// runtime/quiz/render.ts
|
|
@@ -199,6 +199,7 @@ function questionIn(question, lang) {
|
|
|
199
199
|
text: t(option.text),
|
|
200
200
|
correct: option.correct,
|
|
201
201
|
info: t(option.info),
|
|
202
|
+
...option.alt === void 0 ? {} : { alt: t(option.alt) },
|
|
202
203
|
...option.link === void 0 ? {} : { link: link(option.link) }
|
|
203
204
|
}))
|
|
204
205
|
};
|
|
@@ -415,13 +416,17 @@ function questionWords(lang = "de") {
|
|
|
415
416
|
}
|
|
416
417
|
return found;
|
|
417
418
|
}
|
|
418
|
-
function controls(question, { t }, format) {
|
|
419
|
+
function controls(question, { t }, format, describe) {
|
|
419
420
|
const name = questionId(question);
|
|
420
421
|
switch (question.type) {
|
|
421
422
|
case "single-choice":
|
|
422
423
|
case "multiple-choice": {
|
|
423
424
|
const type = question.type === "single-choice" ? "radio" : "checkbox";
|
|
424
|
-
return question.options.map((option, index) =>
|
|
425
|
+
return question.options.map((option, index) => {
|
|
426
|
+
const spoken = option.alt ?? describe?.(option.text);
|
|
427
|
+
const label = spoken === void 0 ? "" : ` aria-label="${escape(spoken)}"`;
|
|
428
|
+
return `<label class="question-option"><input type="${type}" name="${name}" value="${index}"${label}> <span>${format(option.text)}</span></label>`;
|
|
429
|
+
}).join("\n");
|
|
425
430
|
}
|
|
426
431
|
case "true-false": return [true, false].map((value) => `<label class="question-option"><input type="radio" name="${name}" value="${value}"> <span>${t(value ? "true" : "false")}</span></label>`).join("\n");
|
|
427
432
|
case "number": return [
|
|
@@ -456,7 +461,7 @@ function controls(question, { t }, format) {
|
|
|
456
461
|
* what the learner learns about it (the same as its solution). The engine's startQuestion makes it live; without
|
|
457
462
|
* JavaScript the button stays hidden.
|
|
458
463
|
*/
|
|
459
|
-
function renderQuestion(question, { element = "div", next = false, lang = "de", textHtml, materialHtml, explanationHtml, format = escape } = {}) {
|
|
464
|
+
function renderQuestion(question, { element = "div", next = false, lang = "de", textHtml, materialHtml, explanationHtml, format = escape, describe } = {}) {
|
|
460
465
|
const words = questionWords(lang);
|
|
461
466
|
const { t } = words;
|
|
462
467
|
const id = questionId(question);
|
|
@@ -467,7 +472,7 @@ function renderQuestion(question, { element = "div", next = false, lang = "de",
|
|
|
467
472
|
`<fieldset>`,
|
|
468
473
|
`<legend class="question-text">${textHtml ?? format(question.text)}</legend>`,
|
|
469
474
|
...materialHtml === void 0 || materialHtml === "" ? [] : [`<div class="question-material">${materialHtml}</div>`],
|
|
470
|
-
controls(question, words, format),
|
|
475
|
+
controls(question, words, format, describe),
|
|
471
476
|
`</fieldset>`,
|
|
472
477
|
`<div class="question-feedback" role="status"></div>`,
|
|
473
478
|
`<div class="question-reveal" hidden>`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { x as rightEntries } from "./render-
|
|
2
|
-
import { w as message } from "./assessment-
|
|
1
|
+
import { x as rightEntries } from "./render-GS1Htlu8.mjs";
|
|
2
|
+
import { w as message } from "./assessment-Dy359O1O.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 { c as textLanguages } from "./html-mffkHGHh.mjs";
|
|
2
|
-
import { n as checkMaterial, t as MATERIAL_DEFINITIONS } from "./validate-
|
|
3
|
-
import { _ as questionIn, h as questionAddresses, l as isComplete, m as parseNumber, u as isCorrect, v as questionTexts, x as rightEntries } from "./render-
|
|
2
|
+
import { n as checkMaterial, t as MATERIAL_DEFINITIONS } from "./validate-Bxy9NfB8.mjs";
|
|
3
|
+
import { _ as questionIn, h as questionAddresses, l as isComplete, m as parseNumber, u as isCorrect, v as questionTexts, x as rightEntries } from "./render-GS1Htlu8.mjs";
|
|
4
4
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
5
5
|
// runtime/shared/examples.ts
|
|
6
6
|
/**
|
|
@@ -591,6 +591,10 @@ var question_v1_schema_default = {
|
|
|
591
591
|
"description": "One to three sentences of background on this option itself, true whether or not it is the right answer, e.g. \"Der Uluru ist ein Inselberg in der Wüste Zentralaustraliens ...\". After an answer it shows for every option. For a wrong option it is also the reason a learner who chose it reads with the feedback, so say there why it does not fit.",
|
|
592
592
|
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
593
593
|
},
|
|
594
|
+
"alt": {
|
|
595
|
+
"description": "Optional: the option as it is spoken, for the name of its radio button or checkbox, when its text is only a formula, e.g. \"zwei hoch sieben\" for \"$2^7$\". Without it the name is made from the formula in the page's language (\"2 hoch 7\"); write it where that reads badly, e.g. for a chemical formula. Not needed when the text has words.",
|
|
596
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
597
|
+
},
|
|
594
598
|
"link": {
|
|
595
599
|
"description": "Further reading on this option.",
|
|
596
600
|
"$ref": "#/$defs/link"
|
|
@@ -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-
|
|
3
|
+
import { t as checkText } from "./check-CteIFN96.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 = {
|
package/explainer/package.json
CHANGED
package/interactive/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lernapps/app-templates",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "The templates of the archetypes of lernapps.net apps, and the runtime of each archetype that apps depend on (the quiz: engine, build step, styles, end-to-end tests, the schema of the question bank, the questions it shares with the explainer are in ./questions; the explainer: build step, client module with its exercises, the islands of its Vue components, styles, end-to-end tests, the schema of its content; the interactive archetype: build step, the start page and the app page around the app's own Vue components and core, its playthroughs, styles, end-to-end tests, the schemas of its start page and its playthroughs)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
"highlight.js": "11.12.0",
|
|
101
101
|
"marked": "18.1.2",
|
|
102
102
|
"source-map-js": "1.2.2",
|
|
103
|
+
"speech-rule-engine": "4.1.5",
|
|
103
104
|
"temml": "0.14.0",
|
|
104
105
|
"vue": "3.5.44",
|
|
105
106
|
"yaml": "2.9.1"
|
package/quiz/package.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { renderQuestion } from "../shared/questions/render.ts";
|
|
13
13
|
import { type MaterialContext, renderMaterial } from "../shared/blocks/index.ts";
|
|
14
14
|
import { plain, renderWithFormulas } from "../shared/text/markdown.ts";
|
|
15
|
+
import { formulaAlternative } from "../shared/text/speech.ts";
|
|
15
16
|
import {
|
|
16
17
|
assessments,
|
|
17
18
|
drawsTasks,
|
|
@@ -117,6 +118,7 @@ function itemHtml(context: AssessmentContext, data: AssessmentItemData, n: numbe
|
|
|
117
118
|
return `${label}${renderQuestion(question, {
|
|
118
119
|
lang,
|
|
119
120
|
format: (text) => rendered[text] ?? renderWithFormulas(text),
|
|
121
|
+
describe: (text) => formulaAlternative(text, lang),
|
|
120
122
|
...(question.material === undefined ? {} : { materialHtml: renderMaterial(question.material, context.material) }),
|
|
121
123
|
})}`;
|
|
122
124
|
}
|
package/runtime/explainer/e2e.ts
CHANGED
|
@@ -67,6 +67,7 @@ import { message as messageOf } from "./messages.ts";
|
|
|
67
67
|
import { ASSESSMENT_TASK_EVENT, nextTaskNumber, taskNumber } from "./tasks.ts";
|
|
68
68
|
import {
|
|
69
69
|
outline,
|
|
70
|
+
shownText,
|
|
70
71
|
type AssessmentOutline,
|
|
71
72
|
type ComponentOutline,
|
|
72
73
|
type ExerciseOutline,
|
|
@@ -157,6 +158,12 @@ const BAND_WORDS = {
|
|
|
157
158
|
unasked: "assessmentUnasked",
|
|
158
159
|
} as const;
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* `text` without space. A browser's text of a formula has space between its parts ("2 7" for 2⁷) where the content's
|
|
163
|
+
* has none, so texts with formulas are compared without it (in the order of their characters, which is what counts).
|
|
164
|
+
*/
|
|
165
|
+
const squash = (text: string): string => text.replace(/\s+/g, "");
|
|
166
|
+
|
|
160
167
|
/** The task number of the deep link the tests open a generated exercise with. */
|
|
161
168
|
const SEED = 7;
|
|
162
169
|
|
|
@@ -169,7 +176,7 @@ async function expectExercises(page: Page, item: PageOutline): Promise<void> {
|
|
|
169
176
|
const main = page.locator("main");
|
|
170
177
|
const expected = item.exercises.map((exercise) =>
|
|
171
178
|
exercise.kind === "question"
|
|
172
|
-
? { id: exercise.id, texts: [exercise.question.text], answers: [exercise.answers] }
|
|
179
|
+
? { id: exercise.id, texts: [shownText(exercise.question.text)], answers: [exercise.answers] }
|
|
173
180
|
: exercise.kind === "open"
|
|
174
181
|
? { id: exercise.id, texts: [exercise.task], answers: [exercise.solution] }
|
|
175
182
|
: {
|
|
@@ -178,29 +185,31 @@ async function expectExercises(page: Page, item: PageOutline): Promise<void> {
|
|
|
178
185
|
answers: exercise.worked.map((worked) => [worked.answer]),
|
|
179
186
|
},
|
|
180
187
|
);
|
|
181
|
-
const read = () =>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
188
|
+
const read = async () =>
|
|
189
|
+
(
|
|
190
|
+
await main.locator(".explainer-exercise").evaluateAll((elements) =>
|
|
191
|
+
elements.map((element) => {
|
|
192
|
+
const fold = (text: string | null) => (text ?? "").replace(/\s+/g, " ").trim();
|
|
193
|
+
const own = element.classList.contains("explainer-question")
|
|
194
|
+
? [...element.querySelectorAll("legend")]
|
|
195
|
+
: element.classList.contains("explainer-open-task")
|
|
196
|
+
? [...element.querySelectorAll(":scope > .explainer-open-task-text")]
|
|
197
|
+
: [...element.querySelectorAll(".explainer-worked-task > .explainer-task")];
|
|
198
|
+
const solutions = [...element.querySelectorAll<HTMLDetailsElement>("details.explainer-solution")];
|
|
199
|
+
return {
|
|
200
|
+
id: element.id,
|
|
201
|
+
texts: own.map((text) => fold(text.textContent)),
|
|
202
|
+
// what a learner sees of each "Lösung": nothing while it is closed
|
|
203
|
+
shown: solutions.map((details) => (details.open ? fold((details as HTMLElement).innerText) : "")),
|
|
204
|
+
};
|
|
205
|
+
}),
|
|
206
|
+
)
|
|
207
|
+
).map((item) => ({ ...item, texts: item.texts.map(squash), shown: item.shown.map(squash) }));
|
|
199
208
|
const before = await read();
|
|
200
209
|
expect(
|
|
201
210
|
before.map(({ id, texts }) => ({ id, texts })),
|
|
202
211
|
`${item.url}: each exercise with its text`,
|
|
203
|
-
).toEqual(expected.map(({ id, texts }) => ({ id, texts })));
|
|
212
|
+
).toEqual(expected.map(({ id, texts }) => ({ id, texts: texts.map(squash) })));
|
|
204
213
|
// a click on each summary, as a learner opens a <details> (its own behaviour, no script of the page)
|
|
205
214
|
await main
|
|
206
215
|
.locator(".explainer-exercise details.explainer-solution > summary")
|
|
@@ -212,7 +221,7 @@ async function expectExercises(page: Page, item: PageOutline): Promise<void> {
|
|
|
212
221
|
expect(
|
|
213
222
|
after[index]?.shown[at] ?? "",
|
|
214
223
|
`${item.url}: ${exercise.id}, Lösung ${at + 1} shows ${answer}`,
|
|
215
|
-
).toContain(answer);
|
|
224
|
+
).toContain(squash(answer));
|
|
216
225
|
}
|
|
217
226
|
});
|
|
218
227
|
}
|
|
@@ -484,7 +493,9 @@ const feedback = (page: Page, id: string) => page.locator(`[id="${id}"] .questio
|
|
|
484
493
|
function reasonFor(question: Question, answer: Answer): string {
|
|
485
494
|
const quiz = questionMessagesIn("de");
|
|
486
495
|
const [first] = wrongReasons(question, answer);
|
|
487
|
-
if (first !== undefined)
|
|
496
|
+
if (first !== undefined) {
|
|
497
|
+
return first.option === undefined ? shownText(first.text) : `${shownText(first.option)}: ${shownText(first.text)}`;
|
|
498
|
+
}
|
|
488
499
|
return missesRight(question, answer) ? quiz.missingRight : quiz.lookAgain;
|
|
489
500
|
}
|
|
490
501
|
|
|
@@ -495,9 +506,12 @@ function reasonFor(question: Question, answer: Answer): string {
|
|
|
495
506
|
async function wrongThenSolution(page: Page, exercise: string, question: Question, reason: string): Promise<void> {
|
|
496
507
|
const item = page.locator(`[id="${questionId(question)}"]`);
|
|
497
508
|
await expect(feedback(page, questionId(question))).toContainText(questionMessagesIn("de").wrong);
|
|
498
|
-
await expect
|
|
499
|
-
|
|
500
|
-
|
|
509
|
+
await expect
|
|
510
|
+
.poll(
|
|
511
|
+
async () => squash((await item.locator(".question-diagnosis").first().textContent()) ?? ""),
|
|
512
|
+
`${exercise}: the reason of the wrong answer`,
|
|
513
|
+
)
|
|
514
|
+
.toBe(squash(reason));
|
|
501
515
|
await expectFeedbackInView(page, question);
|
|
502
516
|
await expect(item.locator(".question-reveal"), `${exercise}: no solution after a wrong answer`).toBeHidden();
|
|
503
517
|
await expect(item.locator("input, select").first(), `${exercise}: another try`).toBeEnabled();
|
|
@@ -16,6 +16,7 @@ import { questionFor, taskKey } from "./generated/question.ts";
|
|
|
16
16
|
import { WORKED_TASKS } from "./tasks.ts";
|
|
17
17
|
import { type Data, type Generated, type LanguageTemplates, type Templates } from "./generated/types.ts";
|
|
18
18
|
import { plain, renderInline, renderMarkdown, renderWithFormulas } from "../shared/text/markdown.ts";
|
|
19
|
+
import { formulaAlternative } from "../shared/text/speech.ts";
|
|
19
20
|
import { distinctSeeds } from "./tasks.ts";
|
|
20
21
|
import { message } from "./messages.ts";
|
|
21
22
|
|
|
@@ -166,6 +167,7 @@ function questionExercise(block: QuestionBlock, lang: string, material: Material
|
|
|
166
167
|
renderQuestion(question, {
|
|
167
168
|
lang,
|
|
168
169
|
format: renderWithFormulas,
|
|
170
|
+
describe: (text) => formulaAlternative(text, lang),
|
|
169
171
|
...(question.material === undefined ? {} : { materialHtml: renderMaterial(question.material, material) }),
|
|
170
172
|
}),
|
|
171
173
|
`<details class="explainer-solution"><summary>${plain(questionMessagesIn(lang).solution)}</summary>`,
|
|
@@ -8,6 +8,7 @@ import { answerText } from "../shared/questions/render.ts";
|
|
|
8
8
|
import { inLanguage, timelineEvents, type Block, type ComponentExample, type Site, type Text } from "./content.ts";
|
|
9
9
|
import { dataOf, renderExercise, templatesOf, workedTasks } from "./exercises.ts";
|
|
10
10
|
import { kindOf } from "./generated/comparator.ts";
|
|
11
|
+
import { renderWithFormulas } from "../shared/text/markdown.ts";
|
|
11
12
|
import { plainText } from "./generated/texts.ts";
|
|
12
13
|
import { type Data, type Generated, type Templates } from "./generated/types.ts";
|
|
13
14
|
import { loadSite } from "./load.ts";
|
|
@@ -48,6 +49,12 @@ export interface ComponentOutline {
|
|
|
48
49
|
examples?: { accepted?: ComponentExample; rejected?: ComponentExample };
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* A learner text of a question as the page shows it, as text: its formulas $…$ rendered (a formula has no space the
|
|
54
|
+
* tests could rely on in a browser's text, see squash in e2e.ts), the rest as written.
|
|
55
|
+
*/
|
|
56
|
+
export const shownText = (text: string): string => plainText(renderWithFormulas(text));
|
|
57
|
+
|
|
51
58
|
/** An exercise of a page, in the page's language: what it shows and what it accepts. */
|
|
52
59
|
export type ExerciseOutline =
|
|
53
60
|
| {
|
|
@@ -244,7 +251,7 @@ function walk(
|
|
|
244
251
|
question,
|
|
245
252
|
answers:
|
|
246
253
|
question.type === "single-choice" || question.type === "multiple-choice"
|
|
247
|
-
? question.options.filter((option) => option.correct).map((option) => option.text)
|
|
254
|
+
? question.options.filter((option) => option.correct).map((option) => shownText(option.text))
|
|
248
255
|
: [plainText(answerText(question, lang))],
|
|
249
256
|
});
|
|
250
257
|
}
|
|
@@ -20,6 +20,8 @@ export interface Option<T extends Text = string> {
|
|
|
20
20
|
text: T;
|
|
21
21
|
correct: boolean;
|
|
22
22
|
info: T;
|
|
23
|
+
/** The option spoken, as its radio or checkbox is named: for an option that is only a formula; else made from it. */
|
|
24
|
+
alt?: T;
|
|
23
25
|
link?: Link<T>;
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -209,6 +211,7 @@ export function questionIn(question: Question<Text>, lang: string): Question {
|
|
|
209
211
|
text: t(option.text),
|
|
210
212
|
correct: option.correct,
|
|
211
213
|
info: t(option.info),
|
|
214
|
+
...(option.alt === undefined ? {} : { alt: t(option.alt) }),
|
|
212
215
|
...(option.link === undefined ? {} : { link: link(option.link) }),
|
|
213
216
|
})),
|
|
214
217
|
};
|
|
@@ -190,6 +190,10 @@
|
|
|
190
190
|
"description": "One to three sentences of background on this option itself, true whether or not it is the right answer, e.g. \"Der Uluru ist ein Inselberg in der Wüste Zentralaustraliens ...\". After an answer it shows for every option. For a wrong option it is also the reason a learner who chose it reads with the feedback, so say there why it does not fit.",
|
|
191
191
|
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
192
192
|
},
|
|
193
|
+
"alt": {
|
|
194
|
+
"description": "Optional: the option as it is spoken, for the name of its radio button or checkbox, when its text is only a formula, e.g. \"zwei hoch sieben\" for \"$2^7$\". Without it the name is made from the formula in the page's language (\"2 hoch 7\"); write it where that reads badly, e.g. for a chemical formula. Not needed when the text has words.",
|
|
195
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
196
|
+
},
|
|
193
197
|
"link": {
|
|
194
198
|
"description": "Further reading on this option.",
|
|
195
199
|
"$ref": "#/$defs/link"
|
|
@@ -44,17 +44,22 @@ export function questionWords(lang = "de"): Words {
|
|
|
44
44
|
/** How a learner text becomes HTML. */
|
|
45
45
|
export type Format = (text: string) => string;
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
/** A text as words for a name, where the text itself has none (a formula); undefined where it has. */
|
|
48
|
+
export type Describe = (text: string) => string | undefined;
|
|
49
|
+
|
|
50
|
+
function controls(question: Question, { t }: Words, format: Format, describe?: Describe): string {
|
|
48
51
|
const name = questionId(question);
|
|
49
52
|
switch (question.type) {
|
|
50
53
|
case "single-choice":
|
|
51
54
|
case "multiple-choice": {
|
|
52
55
|
const type = question.type === "single-choice" ? "radio" : "checkbox";
|
|
53
56
|
return question.options
|
|
54
|
-
.map(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
.map((option, index) => {
|
|
58
|
+
// an option that is only a formula has no text to name its control; the page says in words what it shows
|
|
59
|
+
const spoken = option.alt ?? describe?.(option.text);
|
|
60
|
+
const label = spoken === undefined ? "" : ` aria-label="${escape(spoken)}"`;
|
|
61
|
+
return `<label class="question-option"><input type="${type}" name="${name}" value="${index}"${label}> <span>${format(option.text)}</span></label>`;
|
|
62
|
+
})
|
|
58
63
|
.join("\n");
|
|
59
64
|
}
|
|
60
65
|
case "true-false":
|
|
@@ -111,6 +116,8 @@ function controls(question: Question, { t }: Words, format: Format): string {
|
|
|
111
116
|
export interface RenderQuestionOptions {
|
|
112
117
|
/** How a learner text becomes HTML, e.g. with its formulas; escaped as plain text by default. */
|
|
113
118
|
format?: (text: string) => string;
|
|
119
|
+
/** The words a screen reader names the control of an option by, when its text is only a formula. */
|
|
120
|
+
describe?: Describe;
|
|
114
121
|
element?: "div" | "li";
|
|
115
122
|
next?: boolean;
|
|
116
123
|
lang?: string;
|
|
@@ -142,6 +149,7 @@ export function renderQuestion(
|
|
|
142
149
|
materialHtml,
|
|
143
150
|
explanationHtml,
|
|
144
151
|
format = escape,
|
|
152
|
+
describe,
|
|
145
153
|
}: RenderQuestionOptions = {},
|
|
146
154
|
): string {
|
|
147
155
|
const words = questionWords(lang);
|
|
@@ -156,7 +164,7 @@ export function renderQuestion(
|
|
|
156
164
|
...(materialHtml === undefined || materialHtml === ""
|
|
157
165
|
? []
|
|
158
166
|
: [`<div class="question-material">${materialHtml}</div>`]),
|
|
159
|
-
controls(question, words, format),
|
|
167
|
+
controls(question, words, format, describe),
|
|
160
168
|
`</fieldset>`,
|
|
161
169
|
`<div class="question-feedback" role="status"></div>`,
|
|
162
170
|
`<div class="question-reveal" hidden>`,
|
|
@@ -158,7 +158,7 @@ export function renderInline(source: string, resolve: (href: string) => string):
|
|
|
158
158
|
/** A plain text as HTML. */
|
|
159
159
|
export const plain = escape;
|
|
160
160
|
|
|
161
|
-
const FORMULA = /\$((?:\\.|[^\\$\n])+?)\$/g;
|
|
161
|
+
export const FORMULA = /\$((?:\\.|[^\\$\n])+?)\$/g;
|
|
162
162
|
|
|
163
163
|
/** A plain text as HTML in which only $…$ is a formula (MathML); everything else stays text, as in a quiz. */
|
|
164
164
|
export function renderWithFormulas(text: string): string {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// The text alternative of a formula, for a name a screen reader or the accessibility check can read where the page has
|
|
2
|
+
// only MathML (an option of a choice whose text is a formula, see the label of its radio): Speech Rule Engine speaks
|
|
3
|
+
// the MathML of the formula in the page's language. Build time only; the engine loads its rules for every language
|
|
4
|
+
// of the runtime once, here, so that speaking a formula in any of them is then synchronous.
|
|
5
|
+
// @ts-expect-error speech-rule-engine ships no types; the part this file uses is typed below (drop this line if it does)
|
|
6
|
+
import untyped from "speech-rule-engine";
|
|
7
|
+
import { isLanguage, LANGUAGES } from "../languages.ts";
|
|
8
|
+
import { renderFormula } from "./formula.ts";
|
|
9
|
+
import { FORMULA } from "./markdown.ts";
|
|
10
|
+
|
|
11
|
+
/** The part of Speech Rule Engine that this file uses. */
|
|
12
|
+
interface Engine {
|
|
13
|
+
/** Loads the rules of the locale; once loaded, switching to it takes effect at once. */
|
|
14
|
+
setupEngine(setup: { locale: string; domain: string; modality: string }): Promise<void>;
|
|
15
|
+
/** The words of a MathML string, in the locale set up last. */
|
|
16
|
+
toSpeech(mathml: string): string;
|
|
17
|
+
}
|
|
18
|
+
const SRE: Engine = untyped;
|
|
19
|
+
|
|
20
|
+
const SPEECH = { domain: "clearspeak", modality: "speech" } as const;
|
|
21
|
+
|
|
22
|
+
for (const locale of LANGUAGES) await SRE.setupEngine({ locale, ...SPEECH });
|
|
23
|
+
|
|
24
|
+
/** The formula `tex` as words in `lang` (German for a language the runtime has no texts for), e.g. "2 hoch 7". */
|
|
25
|
+
export function speakFormula(tex: string, lang: string): string {
|
|
26
|
+
// the rules of every language are loaded: switching to one takes effect at once
|
|
27
|
+
void SRE.setupEngine({ locale: isLanguage(lang) ? lang : "de", ...SPEECH });
|
|
28
|
+
return SRE.toSpeech(renderFormula(tex, false)).replace(/\s+/g, " ").trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The words of a text that is only formulas, $…$ (and space); undefined for any other text, which has words of its own
|
|
33
|
+
* for a name to be made of.
|
|
34
|
+
*/
|
|
35
|
+
export function formulaAlternative(text: string, lang: string): string | undefined {
|
|
36
|
+
const formulas = [...text.matchAll(FORMULA)];
|
|
37
|
+
if (formulas.length === 0 || text.replace(FORMULA, "").trim() !== "") return undefined;
|
|
38
|
+
return formulas.map((formula) => speakFormula(formula[1] ?? "", lang)).join(" ");
|
|
39
|
+
}
|