@lernapps/app-templates 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +195 -2
- package/dist/appname-Bh23cUuz.mjs +12 -0
- package/dist/assessment-By2GGTJC.mjs +1696 -0
- package/dist/block-BK_ohsH1.mjs +82 -0
- package/dist/blocks-BmRrD7UY.mjs +149 -0
- package/dist/blocks.mjs +4 -0
- package/dist/check-BkEl-m-F.mjs +254 -0
- package/dist/content-cYBPYg_J.mjs +140 -0
- package/dist/contract-PONpxJtd.mjs +52 -0
- package/dist/e2e-BiMI-pCL.mjs +89 -0
- package/dist/engine-BqF2Vffn.mjs +136 -0
- package/dist/explainer-e2e.mjs +1297 -0
- package/dist/explainer-islands.mjs +197 -0
- package/dist/explainer-plugin.mjs +2131 -0
- package/dist/explainer.mjs +1246 -0
- package/dist/html-mffkHGHh.mjs +89 -0
- package/dist/image-CspJVfyQ.mjs +21 -0
- package/dist/interactive-e2e.mjs +140 -0
- package/dist/interactive-plugin.mjs +242 -0
- package/dist/interactive.mjs +75 -0
- package/dist/load-C-0hUaSC.mjs +16947 -0
- package/dist/needs-BzEahJ6X.mjs +97 -0
- package/dist/pages-9RaaomoO.mjs +81 -0
- package/dist/questions-e2e.mjs +1 -0
- package/dist/questions-engine.mjs +3 -0
- package/dist/questions.mjs +6 -0
- package/dist/quiz-BKc8JYtC.mjs +70 -0
- package/dist/quiz-e2e.mjs +174 -0
- package/dist/quiz-plugin.mjs +266 -0
- package/dist/quiz.mjs +246 -0
- package/dist/render-BJey0bwl.mjs +540 -0
- package/dist/render-BL6fV_NI.mjs +507 -0
- package/dist/render-DpRO_RKm.mjs +146 -0
- package/dist/result-_eQcamoz.mjs +180 -0
- package/dist/tasks-DADUYy52.mjs +74 -0
- package/dist/validate-COaZso7u.mjs +1072 -0
- package/dist/validate-CxtdeQ5S.mjs +146 -0
- package/dist/vue-DO1m6q1R.mjs +40 -0
- package/dist/vue-code-BkJjW_I_.mjs +320 -0
- package/docs/arc42/01-introduction-and-goals.arc42.md +82 -0
- package/docs/arc42/02-constraints.arc42.md +228 -0
- package/docs/arc42/03-system-context.arc42.md +118 -0
- package/docs/arc42/04-solution-strategy.arc42.md +47 -0
- package/docs/arc42/05-building-blocks.arc42.md +307 -0
- package/docs/arc42/06-runtime-view.arc42.md +98 -0
- package/docs/arc42/07-deployment-view.arc42.md +160 -0
- package/docs/arc42/08-concepts.arc42.md +123 -0
- package/docs/arc42/09-decisions.arc42.md +246 -0
- package/docs/arc42/10-quality-requirements.arc42.md +155 -0
- package/docs/arc42/11-risks.arc42.md +113 -0
- package/docs/arc42/12-glossary.arc42.md +162 -0
- package/docs/arc42/architecture-evidence.md +395 -0
- package/explainer/.arc42ignore +3 -0
- package/explainer/README.md +54 -0
- package/explainer/_gitignore +5 -0
- package/explainer/docs/arc42/03-explainer-system-context.arc42.md +35 -0
- package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +659 -0
- package/explainer/docs/arc42/06-explainer-runtime-view.arc42.md +105 -0
- package/explainer/docs/arc42/07-explainer-deployment-view.arc42.md +34 -0
- package/explainer/docs/arc42/08-explainer-concepts.arc42.md +17 -0
- package/explainer/docs/arc42/09-explainer-decisions.arc42.md +443 -0
- package/explainer/docs/arc42/10-explainer-quality-requirements.arc42.md +54 -0
- package/explainer/docs/arc42/11-explainer-risks.arc42.md +17 -0
- package/explainer/docs/arc42/12-explainer-glossary.arc42.md +185 -0
- package/explainer/e2e/explainer.e2e.ts +3 -0
- package/explainer/index.html +11 -0
- package/explainer/package.json +18 -0
- package/explainer/playwright.config.ts +1 -0
- package/explainer/src/app.yaml +20 -0
- package/explainer/src/components/ProzentFeld.test.ts +30 -0
- package/explainer/src/components/ProzentFeld.ts +121 -0
- package/explainer/src/components/ProzentFeld.vue +84 -0
- package/explainer/src/images/hunderterfeld-klammern.svg +105 -0
- package/explainer/src/main.ts +4 -0
- package/explainer/src/prozent/chapter.yaml +18 -0
- package/explainer/src/prozent/grundwert.yaml +115 -0
- package/explainer/src/prozent/prozentsatz.yaml +99 -0
- package/explainer/src/prozent/prozentwert.yaml +119 -0
- package/explainer/tsconfig.json +1 -0
- package/explainer/vite.config.ts +4 -0
- package/interactive/.arc42ignore +5 -0
- package/interactive/README.md +28 -0
- package/interactive/_gitignore +4 -0
- package/interactive/docs/arc42/05-interactive-building-blocks.arc42.md +518 -0
- package/interactive/docs/arc42/06-interactive-runtime-view.arc42.md +58 -0
- package/interactive/docs/arc42/07-interactive-deployment-view.arc42.md +35 -0
- package/interactive/docs/arc42/08-interactive-concepts.arc42.md +52 -0
- package/interactive/docs/arc42/09-interactive-decisions.arc42.md +130 -0
- package/interactive/docs/arc42/10-interactive-quality-requirements.arc42.md +45 -0
- package/interactive/docs/arc42/11-interactive-risks.arc42.md +20 -0
- package/interactive/docs/arc42/12-interactive-glossary.arc42.md +80 -0
- package/interactive/e2e/interactive.e2e.ts +3 -0
- package/interactive/index.html +11 -0
- package/interactive/package.json +18 -0
- package/interactive/playwright.config.ts +1 -0
- package/interactive/src/app.yaml +28 -0
- package/interactive/src/core/index.ts +101 -0
- package/interactive/src/data/lessons.json +4 -0
- package/interactive/src/env.d.ts +6 -0
- package/interactive/src/main.ts +6 -0
- package/interactive/src/messages/de.json +12 -0
- package/interactive/src/playthroughs/grundreihe.yaml +33 -0
- package/interactive/src/playthroughs/obere-reihe.yaml +14 -0
- package/interactive/src/ui/App.vue +85 -0
- package/interactive/tsconfig.json +1 -0
- package/interactive/vite.config.ts +4 -0
- package/package.json +142 -4
- package/quiz/README.md +31 -0
- package/quiz/_gitignore +5 -0
- package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +283 -0
- package/quiz/docs/arc42/06-quiz-runtime-view.arc42.md +21 -0
- package/quiz/docs/arc42/07-quiz-deployment-view.arc42.md +33 -0
- package/quiz/docs/arc42/09-quiz-decisions.arc42.md +36 -0
- package/quiz/docs/arc42/10-quiz-quality-requirements.arc42.md +53 -0
- package/quiz/docs/arc42/11-quiz-risks.arc42.md +19 -0
- package/quiz/docs/arc42/12-quiz-glossary.arc42.md +27 -0
- package/quiz/e2e/quiz.e2e.ts +3 -0
- package/quiz/index.html +11 -0
- package/quiz/package.json +18 -0
- package/quiz/playwright.config.ts +1 -0
- package/quiz/src/images/berge-ordnen.jpg +0 -0
- package/quiz/src/images/everest-hoehe.jpg +0 -0
- package/quiz/src/images/galapagos.jpg +0 -0
- package/quiz/src/images/grand-canyon.jpg +0 -0
- package/quiz/src/images/totes-meer.jpg +0 -0
- package/quiz/src/images/uluru.jpg +0 -0
- package/quiz/src/images/wasserfaelle-laender.jpg +0 -0
- package/quiz/src/main.ts +4 -0
- package/quiz/src/quiz.json +312 -0
- package/quiz/tsconfig.json +1 -0
- package/quiz/vite.config.ts +4 -0
- package/runtime/explainer/assessment-client.ts +478 -0
- package/runtime/explainer/assessment-page.ts +446 -0
- package/runtime/explainer/assessment.ts +393 -0
- package/runtime/explainer/check-page.ts +520 -0
- package/runtime/explainer/components/exercise.ts +409 -0
- package/runtime/explainer/components/props.ts +230 -0
- package/runtime/explainer/components/read.ts +428 -0
- package/runtime/explainer/content.ts +277 -0
- package/runtime/explainer/e2e.ts +1474 -0
- package/runtime/explainer/exercises.ts +324 -0
- package/runtime/explainer/explainer.css +1283 -0
- package/runtime/explainer/explainer.ts +838 -0
- package/runtime/explainer/explainer.v1.schema.json +1165 -0
- package/runtime/explainer/expression.ts +407 -0
- package/runtime/explainer/fraction.ts +107 -0
- package/runtime/explainer/generated/comparator.ts +31 -0
- package/runtime/explainer/generated/question.ts +165 -0
- package/runtime/explainer/generated/soundness.ts +280 -0
- package/runtime/explainer/generated/texts.ts +257 -0
- package/runtime/explainer/generated/types.ts +103 -0
- package/runtime/explainer/generated/values.ts +136 -0
- package/runtime/explainer/islands.ts +281 -0
- package/runtime/explainer/load.ts +457 -0
- package/runtime/explainer/messages/de.json +200 -0
- package/runtime/explainer/messages/en.json +200 -0
- package/runtime/explainer/messages.ts +13 -0
- package/runtime/explainer/needs.ts +122 -0
- package/runtime/explainer/open-task.ts +235 -0
- package/runtime/explainer/outline.ts +299 -0
- package/runtime/explainer/plugin.ts +488 -0
- package/runtime/explainer/position.ts +62 -0
- package/runtime/explainer/prompt.ts +75 -0
- package/runtime/explainer/render.ts +686 -0
- package/runtime/explainer/result.ts +238 -0
- package/runtime/explainer/routes.ts +119 -0
- package/runtime/explainer/schema.ts +140 -0
- package/runtime/explainer/scope.ts +87 -0
- package/runtime/explainer/stuck.ts +57 -0
- package/runtime/explainer/tasks.ts +109 -0
- package/runtime/explainer/tutor.ts +469 -0
- package/runtime/explainer/tutorpage.ts +669 -0
- package/runtime/explainer/video.ts +123 -0
- package/runtime/interactive/contract.ts +121 -0
- package/runtime/interactive/e2e.ts +190 -0
- package/runtime/interactive/interactive.css +82 -0
- package/runtime/interactive/interactive.ts +115 -0
- package/runtime/interactive/interactive.v1.schema.json +146 -0
- package/runtime/interactive/load.ts +188 -0
- package/runtime/interactive/messages/de.json +14 -0
- package/runtime/interactive/messages/en.json +14 -0
- package/runtime/interactive/messages.ts +11 -0
- package/runtime/interactive/playthrough.v1.schema.json +143 -0
- package/runtime/interactive/plugin.ts +266 -0
- package/runtime/interactive/render.ts +126 -0
- package/runtime/quiz/e2e.ts +233 -0
- package/runtime/quiz/engine.ts +242 -0
- package/runtime/quiz/messages/de.json +23 -0
- package/runtime/quiz/messages/en.json +23 -0
- package/runtime/quiz/plugin.ts +150 -0
- package/runtime/quiz/quiz.css +154 -0
- package/runtime/quiz/quiz.ts +56 -0
- package/runtime/quiz/quiz.v1.schema.json +72 -0
- package/runtime/quiz/render.ts +173 -0
- package/runtime/quiz/validate.ts +108 -0
- package/runtime/shared/appname.ts +16 -0
- package/runtime/shared/blocks/block.ts +162 -0
- package/runtime/shared/blocks/blocks.css +190 -0
- package/runtime/shared/blocks/blocks.v1.schema.json +432 -0
- package/runtime/shared/blocks/code.ts +26 -0
- package/runtime/shared/blocks/figures.ts +49 -0
- package/runtime/shared/blocks/image.ts +25 -0
- package/runtime/shared/blocks/index.ts +13 -0
- package/runtime/shared/blocks/messages/de.json +4 -0
- package/runtime/shared/blocks/messages/en.json +4 -0
- package/runtime/shared/blocks/render.ts +134 -0
- package/runtime/shared/blocks/validate.ts +165 -0
- package/runtime/shared/content.ts +197 -0
- package/runtime/shared/examples.ts +20 -0
- package/runtime/shared/html.ts +10 -0
- package/runtime/shared/languages.ts +94 -0
- package/runtime/shared/page.css +101 -0
- package/runtime/shared/pages.ts +114 -0
- package/runtime/shared/questions/e2e.ts +115 -0
- package/runtime/shared/questions/engine.ts +209 -0
- package/runtime/shared/questions/index.ts +39 -0
- package/runtime/shared/questions/messages/de.json +26 -0
- package/runtime/shared/questions/messages/en.json +26 -0
- package/runtime/shared/questions/question.ts +430 -0
- package/runtime/shared/questions/question.v1.schema.json +342 -0
- package/runtime/shared/questions/questions.css +192 -0
- package/runtime/shared/questions/render.ts +247 -0
- package/runtime/shared/questions/validate.ts +518 -0
- package/runtime/shared/schema.ts +44 -0
- package/runtime/shared/text/check.ts +68 -0
- package/runtime/shared/text/formula.ts +69 -0
- package/runtime/shared/text/markdown.ts +185 -0
- package/runtime/shared/text/text.v1.schema.json +34 -0
- package/runtime/shared/vue-code.ts +395 -0
- package/runtime/shared/vue.ts +38 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// runtime/explainer/needs.ts
|
|
2
|
+
/** The needs, in the order the start page shows them; their ids are a contract with every tutor. */
|
|
3
|
+
const NEEDS = [
|
|
4
|
+
"kurze-schritte",
|
|
5
|
+
"wenig-lesen",
|
|
6
|
+
"mehr-tiefe",
|
|
7
|
+
"andere-muttersprache",
|
|
8
|
+
"klar-und-woertlich",
|
|
9
|
+
"rechnen-anschaulich"
|
|
10
|
+
];
|
|
11
|
+
/** The need whose choice also asks for the learner's first language. */
|
|
12
|
+
const OTHER_LANGUAGE = "andere-muttersprache";
|
|
13
|
+
/** The key of the choice among the learner's settings, for every explainer or for one app. */
|
|
14
|
+
const NEEDS_KEY = "tutor-bedarf";
|
|
15
|
+
const isNeed = (value) => NEEDS.some((need) => need === value);
|
|
16
|
+
/** Whether a stored value is a choice of needs. */
|
|
17
|
+
function isNeedsChoice(value) {
|
|
18
|
+
if (typeof value !== "object" || value === null) return false;
|
|
19
|
+
const { needs, language } = value;
|
|
20
|
+
return Array.isArray(needs) && needs.every(isNeed) && (language === void 0 || typeof language === "string");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The first languages the start page offers, as language tags: the most common first languages of children in
|
|
24
|
+
* German schools besides German, and German Sign Language (`gsg`).
|
|
25
|
+
*/
|
|
26
|
+
const LANGUAGES = [
|
|
27
|
+
"ar",
|
|
28
|
+
"bg",
|
|
29
|
+
"bs",
|
|
30
|
+
"en",
|
|
31
|
+
"fa",
|
|
32
|
+
"fr",
|
|
33
|
+
"gsg",
|
|
34
|
+
"el",
|
|
35
|
+
"it",
|
|
36
|
+
"hr",
|
|
37
|
+
"ku",
|
|
38
|
+
"pl",
|
|
39
|
+
"ps",
|
|
40
|
+
"pt",
|
|
41
|
+
"ro",
|
|
42
|
+
"ru",
|
|
43
|
+
"so",
|
|
44
|
+
"sq",
|
|
45
|
+
"sr",
|
|
46
|
+
"es",
|
|
47
|
+
"ti",
|
|
48
|
+
"tr",
|
|
49
|
+
"uk",
|
|
50
|
+
"vi",
|
|
51
|
+
"zh"
|
|
52
|
+
];
|
|
53
|
+
/** The names of the languages Intl does not know, in each language of the explainer. */
|
|
54
|
+
const OWN_NAMES = {
|
|
55
|
+
de: { gsg: "Deutsche Gebärdensprache" },
|
|
56
|
+
en: { gsg: "German Sign Language" }
|
|
57
|
+
};
|
|
58
|
+
/** The name of the language `tag` in `lang`, "Türkisch" for `tr` in German; the tag itself if none is known. */
|
|
59
|
+
function languageName(tag, lang) {
|
|
60
|
+
const own = OWN_NAMES[lang]?.[tag] ?? OWN_NAMES["de"]?.[tag];
|
|
61
|
+
if (own !== void 0) return own;
|
|
62
|
+
try {
|
|
63
|
+
const name = new Intl.DisplayNames([lang], {
|
|
64
|
+
type: "language",
|
|
65
|
+
fallback: "none"
|
|
66
|
+
}).of(tag);
|
|
67
|
+
return name === void 0 ? tag : name.charAt(0).toLocaleUpperCase(lang) + name.slice(1);
|
|
68
|
+
} catch {
|
|
69
|
+
return tag;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** The query parameter of the needs, `bedarf=kurze-schritte,mehr-tiefe` in the order of NEEDS; "" for none. */
|
|
73
|
+
function needsParameter(choice) {
|
|
74
|
+
const needs = NEEDS.filter((need) => choice?.needs.includes(need));
|
|
75
|
+
return needs.length === 0 ? "" : `bedarf=${needs.join(",")}`;
|
|
76
|
+
}
|
|
77
|
+
/** The query `query` (with its "?" or "") with the needs of `choice` added. */
|
|
78
|
+
function withNeeds(query, choice) {
|
|
79
|
+
const needs = needsParameter(choice);
|
|
80
|
+
if (needs === "") return query;
|
|
81
|
+
return query === "" ? `?${needs}` : `${query}&${needs}`;
|
|
82
|
+
}
|
|
83
|
+
const FIRST_LANGUAGE = {
|
|
84
|
+
de: "Meine Muttersprache ist {language}.",
|
|
85
|
+
en: "My first language is {language}."
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* The sentence the start prompt adds in the learner's voice when they chose `andere-muttersprache` with a language,
|
|
89
|
+
* in `lang`: "Meine Muttersprache ist Türkisch."; "" otherwise.
|
|
90
|
+
*/
|
|
91
|
+
function languageSentence(choice, lang) {
|
|
92
|
+
if (choice?.language === void 0 || !choice.needs.includes("andere-muttersprache")) return "";
|
|
93
|
+
return (FIRST_LANGUAGE[lang] ?? FIRST_LANGUAGE["de"] ?? "").replace("{language}", languageName(choice.language, lang));
|
|
94
|
+
}
|
|
95
|
+
/** A prompt that starts a chat, with the sentence of the learner's first language after it when they chose one. */
|
|
96
|
+
const withLanguage = (prompt, choice, lang) => [prompt, languageSentence(choice, lang)].filter((part) => part !== "").join("\n");
|
|
97
|
+
export { isNeedsChoice as a, withNeeds as c, OTHER_LANGUAGE as i, NEEDS as n, languageName as o, NEEDS_KEY as r, withLanguage as s, LANGUAGES as t };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// runtime/shared/pages.ts
|
|
2
|
+
/** The root of a language's tree, from the app's root: "" for German, "en/" for English. */
|
|
3
|
+
const languageRoot = (lang) => lang === "de" ? "" : `${lang}/`;
|
|
4
|
+
/** A relative URL from the page at `from` to `to` (both from the app's root). */
|
|
5
|
+
function relative(from, to) {
|
|
6
|
+
const url = `${"../".repeat(from.split("/").filter((part) => part !== "").length)}${to}`;
|
|
7
|
+
return url === "" ? "./" : url.startsWith("#") ? `./${url}` : url;
|
|
8
|
+
}
|
|
9
|
+
/** The built shell's relative URLs (./assets/…, built by Vite with base "./"), from a page at `path`. */
|
|
10
|
+
function fromDepth(html, path) {
|
|
11
|
+
const up = "../".repeat(path.split("/").filter((part) => part !== "").length);
|
|
12
|
+
return html.replace(/((?:src|href)=")\.\/(?!\/)/g, `$1${up === "" ? "./" : up}`);
|
|
13
|
+
}
|
|
14
|
+
/** The page with `lang` as the language of its <html>, in place of the one it had. */
|
|
15
|
+
const withLanguage = (html, lang) => html.replace(/<html([^>]*?)\slang="[^"]*"/, "<html$1").replace("<html", `<html lang="${lang}"`);
|
|
16
|
+
/** An alternate link to the page in each of its languages, for <head>. */
|
|
17
|
+
const alternateLinks = ({ path, local, languages }) => languages.map((lang) => `<link rel="alternate" hreflang="${lang}" href="${relative(path, languageRoot(lang) + local)}" />`);
|
|
18
|
+
/**
|
|
19
|
+
* The header of a page in `lang`: the language switch, a link to the page in each language, named in its own
|
|
20
|
+
* language, the current one marked; nothing while the app has one language. `label` and `name` give HTML, already
|
|
21
|
+
* escaped.
|
|
22
|
+
*/
|
|
23
|
+
function pageHeader(page) {
|
|
24
|
+
const { path, local, languages, lang: current, label, name } = page;
|
|
25
|
+
if (languages.length < 2) return "";
|
|
26
|
+
return `<header class="page-header"><nav class="page-languages" aria-label="${label}"><ul>${languages.map((lang) => `<li><a href="${relative(path, languageRoot(lang) + local)}" hreflang="${lang}" lang="${lang}"${lang === current ? " aria-current=\"page\"" : ""}>${name(lang)}</a></li>`).join("")}</ul></nav></header>`;
|
|
27
|
+
}
|
|
28
|
+
/** The footer of a page: what the app is, and the links to the imprint and the privacy statement; texts as HTML, already escaped. */
|
|
29
|
+
const pageFooter = (words) => `<footer class="page-footer"><p>${words.footer} · <a href="https://lernapps.net/imprint/">${words.imprint}</a> · <a href="https://lernapps.net/privacy/">${words.privacy}</a></p></footer>`;
|
|
30
|
+
/**
|
|
31
|
+
* The page for every address the app does not have, 404.html at the bundle's root: GitHub Pages serves it for any
|
|
32
|
+
* missing path below the site, at that path, so the page cannot know its own depth, and relative links in it would
|
|
33
|
+
* point below the missing path. Its script finds the app's root instead: from the missing path's folder upwards, the
|
|
34
|
+
* first folder whose 404.html exists (one HEAD request each, to the same server) is the root of the bundle that
|
|
35
|
+
* answers there: the app at /<repo>/, a preview at /<repo>/pr-preview/pr-<n>/ (which has its own 404.html), or / when
|
|
36
|
+
* served locally. It then moves on to that root's start page and points the link there. Without JavaScript the link
|
|
37
|
+
* leads to the start page of the site. German, plain language, not indexed, nothing loaded from another server.
|
|
38
|
+
*/
|
|
39
|
+
function notFoundPage() {
|
|
40
|
+
return [
|
|
41
|
+
`<!doctype html>`,
|
|
42
|
+
`<html lang="de">`,
|
|
43
|
+
` <head>`,
|
|
44
|
+
` <meta charset="utf-8" />`,
|
|
45
|
+
` <meta name="viewport" content="width=device-width, initial-scale=1" />`,
|
|
46
|
+
` <meta name="robots" content="noindex" />`,
|
|
47
|
+
` <title>Seite nicht gefunden</title>`,
|
|
48
|
+
` <style>body { margin: 2rem auto; max-width: 40rem; padding: 0 1rem; font-family: system-ui, sans-serif; line-height: 1.5; color: #1a1a1a; background: #ffffff; } a { color: #1d4ed8; }</style>`,
|
|
49
|
+
` </head>`,
|
|
50
|
+
` <body>`,
|
|
51
|
+
` <main>`,
|
|
52
|
+
` <h1>Seite nicht gefunden</h1>`,
|
|
53
|
+
` <p>Diese Seite gibt es nicht (mehr).</p>`,
|
|
54
|
+
` <p id="weiter" hidden>Du wirst zur Startseite der App weitergeleitet.</p>`,
|
|
55
|
+
` <p><a id="start" href="/">Zur Startseite</a></p>`,
|
|
56
|
+
` </main>`,
|
|
57
|
+
` <script>(async () => {
|
|
58
|
+
const link = document.getElementById("start");
|
|
59
|
+
let folder = location.pathname.slice(0, location.pathname.lastIndexOf("/") + 1);
|
|
60
|
+
for (;;) {
|
|
61
|
+
try {
|
|
62
|
+
const found = await fetch(folder + "404.html", { method: "HEAD", cache: "no-store" });
|
|
63
|
+
if (found.ok) break;
|
|
64
|
+
} catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (folder === "/") return;
|
|
68
|
+
folder = folder.slice(0, folder.lastIndexOf("/", folder.length - 2) + 1);
|
|
69
|
+
}
|
|
70
|
+
link.href = folder;
|
|
71
|
+
link.textContent = "Zur Startseite der App";
|
|
72
|
+
if (folder === location.pathname) return;
|
|
73
|
+
document.getElementById("weiter").hidden = false;
|
|
74
|
+
location.replace(folder);
|
|
75
|
+
})();<\/script>`,
|
|
76
|
+
` </body>`,
|
|
77
|
+
`</html>`,
|
|
78
|
+
``
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
81
|
+
export { pageFooter as a, withLanguage as c, notFoundPage as i, fromDepth as n, pageHeader as o, languageRoot as r, relative as s, alternateLinks as t };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t as answerQuestion, n as expectBackground, r as expectFeedbackInView, i as expectMaterial, a as giveAnswer, o as question } from "./e2e-BiMI-pCL.mjs";
|
|
@@ -0,0 +1,6 @@
|
|
|
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-BJey0bwl.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-COaZso7u.mjs";
|
|
3
|
+
// runtime/shared/questions/index.ts
|
|
4
|
+
/** The texts of a question for learners in German, by their key (messages/de.json); questionMessagesIn(lang) for another. */
|
|
5
|
+
const messages = de_default;
|
|
6
|
+
export { QUESTION_MESSAGES, QUESTION_REF, QUESTION_SCHEMA_ID, QUESTION_TYPES, answerText, hash, isComplete, isCorrect, materialPairs, messages, missesRight, mixed, parseNumber, questionAddresses, questionDetails, questionId, questionIn, questionMessagesIn, questionSchema, questionSchemaProblems, questionSchemas, questionTexts, random, renderQuestion, renderSolution, rightAnswer, rightEntries, shuffled, validateQuestion, wrongAnswer, wrongReasons };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { i as inLanguage } from "./html-mffkHGHh.mjs";
|
|
2
|
+
import { _ as questionIn } from "./render-BJey0bwl.mjs";
|
|
3
|
+
// runtime/quiz/messages/de.json
|
|
4
|
+
var de_default = {
|
|
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.",
|
|
6
|
+
noscript: "Ohne JavaScript kannst du alle Fragen lesen. Die Lösungen mit mehr Wissen zu jeder Antwort stehen am Ende der Seite.",
|
|
7
|
+
questions: "Fragen",
|
|
8
|
+
countLabel: "Wie viele Fragen möchtest du beantworten?",
|
|
9
|
+
countOf: "von {total}",
|
|
10
|
+
start: "Los geht’s",
|
|
11
|
+
progress: "Frage {current} von {total}",
|
|
12
|
+
toResult: "Zum Ergebnis",
|
|
13
|
+
result: "Ergebnis",
|
|
14
|
+
score: "Du hast {score} von {total} Fragen richtig beantwortet.",
|
|
15
|
+
last: "Beim letzten Mal waren es {score} von {total}.",
|
|
16
|
+
restart: "Noch einmal",
|
|
17
|
+
solutions: "Lösungen",
|
|
18
|
+
toQuestion: "Zur Frage",
|
|
19
|
+
footer: "Ein Quiz auf lernapps.net",
|
|
20
|
+
imprint: "Impressum",
|
|
21
|
+
privacy: "Datenschutz",
|
|
22
|
+
sources: "Quellen",
|
|
23
|
+
sourcesIntro: "Die Fakten in diesem Quiz stammen aus diesen Quellen:",
|
|
24
|
+
language: "Deutsch",
|
|
25
|
+
languages: "Sprache"
|
|
26
|
+
};
|
|
27
|
+
// runtime/quiz/messages/en.json
|
|
28
|
+
var en_default = {
|
|
29
|
+
howto: "Answer the questions one by one. After each answer you see whether it is right and learn more about every answer. At the end you see your result.",
|
|
30
|
+
noscript: "Without JavaScript you can read all questions. The solutions, with more on every answer, are at the end of the page.",
|
|
31
|
+
questions: "Questions",
|
|
32
|
+
countLabel: "How many questions do you want to answer?",
|
|
33
|
+
countOf: "of {total}",
|
|
34
|
+
start: "Start",
|
|
35
|
+
progress: "Question {current} of {total}",
|
|
36
|
+
toResult: "To the result",
|
|
37
|
+
result: "Result",
|
|
38
|
+
score: "You answered {score} of {total} questions right.",
|
|
39
|
+
last: "Last time it was {score} of {total}.",
|
|
40
|
+
restart: "Again",
|
|
41
|
+
solutions: "Solutions",
|
|
42
|
+
toQuestion: "To the question",
|
|
43
|
+
footer: "A quiz on lernapps.net",
|
|
44
|
+
imprint: "Imprint",
|
|
45
|
+
privacy: "Privacy",
|
|
46
|
+
sources: "Sources",
|
|
47
|
+
sourcesIntro: "The facts in this quiz come from these sources:",
|
|
48
|
+
language: "English",
|
|
49
|
+
languages: "Language"
|
|
50
|
+
};
|
|
51
|
+
// runtime/quiz/quiz.ts
|
|
52
|
+
/** The quiz's languages, German first. */
|
|
53
|
+
const quizLanguages = (quiz) => quiz.languages ?? ["de"];
|
|
54
|
+
/** The quiz in `lang`: each text from its map, a plain string as it is; everything else as it is. */
|
|
55
|
+
function quizIn(quiz, lang) {
|
|
56
|
+
const { sources } = quiz;
|
|
57
|
+
return {
|
|
58
|
+
...quiz,
|
|
59
|
+
title: inLanguage(quiz.title, lang),
|
|
60
|
+
intro: inLanguage(quiz.intro, lang),
|
|
61
|
+
questions: quiz.questions.map((question) => questionIn(question, lang)),
|
|
62
|
+
...sources === void 0 ? {} : { sources: sources.map((source) => ({
|
|
63
|
+
...source,
|
|
64
|
+
title: inLanguage(source.title, lang)
|
|
65
|
+
})) }
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** The id of the element of the solution of a question, in the list of solutions. */
|
|
69
|
+
const solutionId = (question) => `loesung-${question.id}`;
|
|
70
|
+
export { de_default as a, en_default as i, quizLanguages as n, solutionId as r, quizIn as t };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { C as wrongAnswer, b as rightAnswer, g as questionId } from "./render-BJey0bwl.mjs";
|
|
2
|
+
import { i as expectMaterial, n as expectBackground, o as question, r as expectFeedbackInView, t as answerQuestion } from "./e2e-BiMI-pCL.mjs";
|
|
3
|
+
import { n as quizLanguages, t as quizIn } from "./quiz-BKc8JYtC.mjs";
|
|
4
|
+
import { n as messagesIn, t as QUIZ_MESSAGES } from "./render-DpRO_RKm.mjs";
|
|
5
|
+
import { expect, test } from "playwright/test";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
// runtime/quiz/e2e.ts
|
|
8
|
+
const de = QUIZ_MESSAGES.de;
|
|
9
|
+
/** The quiz's own texts in `lang`. */
|
|
10
|
+
const messagesOf = messagesIn;
|
|
11
|
+
/** The quiz's tests, for the question bank in `bank` (relative to the app's root). */
|
|
12
|
+
function quizTests(bank = "src/quiz.json") {
|
|
13
|
+
const all = JSON.parse(readFileSync(bank, "utf8"));
|
|
14
|
+
const languages = quizLanguages(all);
|
|
15
|
+
const quiz = quizIn(all, "de");
|
|
16
|
+
test.describe("quiz", () => {
|
|
17
|
+
test.use({ reducedMotion: "reduce" });
|
|
18
|
+
for (const item of quiz.questions) {
|
|
19
|
+
test(`${item.id} (${item.type}): its link opens it and the right answer counts`, async ({ page }) => {
|
|
20
|
+
await page.goto(`./#${questionId(item)}`);
|
|
21
|
+
await expect(question(page, item)).toBeVisible();
|
|
22
|
+
await expect(page.locator(".question:visible")).toHaveCount(1);
|
|
23
|
+
await answerQuestion(page, item, rightAnswer(item));
|
|
24
|
+
await expect(question(page, item).locator(".question-feedback")).toContainText(de.right);
|
|
25
|
+
await expectFeedbackInView(page, item);
|
|
26
|
+
const next = question(page, item).locator(".question-next");
|
|
27
|
+
for (let tab = 0; tab < 30 && !await next.evaluate((button) => button === document.activeElement); tab++) await page.keyboard.press("Tab");
|
|
28
|
+
await expect(next).toBeFocused();
|
|
29
|
+
const reveal = question(page, item).locator(".question-reveal");
|
|
30
|
+
await expectBackground(reveal, item);
|
|
31
|
+
if (item.type === "single-choice" || item.type === "multiple-choice") {
|
|
32
|
+
const right = item.options.filter((option) => option.correct).length;
|
|
33
|
+
await expect(reveal.locator(".question-badge-right")).toHaveCount(right);
|
|
34
|
+
await expect(reveal.locator(".question-badge-right").first()).toHaveText(de.rightOption);
|
|
35
|
+
} else await expect(reveal).toContainText(de.rightAnswer);
|
|
36
|
+
});
|
|
37
|
+
if (item.material !== void 0) test(`${item.id} (${item.type}): what it shows is there, and its images load`, async ({ page }) => {
|
|
38
|
+
await page.goto(`./#${questionId(item)}`);
|
|
39
|
+
await expectMaterial(page, item);
|
|
40
|
+
});
|
|
41
|
+
test(`${item.id} (${item.type}): a wrong answer does not count`, async ({ page }) => {
|
|
42
|
+
await page.goto(`./#${questionId(item)}`);
|
|
43
|
+
await answerQuestion(page, item, wrongAnswer(item));
|
|
44
|
+
await expect(question(page, item).locator(".question-feedback")).toContainText(de.wrong);
|
|
45
|
+
await expectFeedbackInView(page, item);
|
|
46
|
+
await expectBackground(question(page, item).locator(".question-reveal"), item);
|
|
47
|
+
if (item.type === "single-choice" || item.type === "multiple-choice") await expect(question(page, item).locator(".question-badge-chosen").first()).toHaveText(de.yourChoice);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
test("the seed in the address gives the order; without one, the address gets one", async ({ page }) => {
|
|
51
|
+
const everything = `&n=${quiz.questions.length}`;
|
|
52
|
+
const order = async (address) => {
|
|
53
|
+
await page.goto(address);
|
|
54
|
+
return page.locator(".question").evaluateAll((items) => items.map((item) => item.id).join());
|
|
55
|
+
};
|
|
56
|
+
const first = await order(`./?seed=11${everything}`);
|
|
57
|
+
expect(await order(`./?seed=11${everything}`)).toBe(first);
|
|
58
|
+
if (quiz.questions.length > 2) {
|
|
59
|
+
const others = [];
|
|
60
|
+
for (const seed of [
|
|
61
|
+
12,
|
|
62
|
+
13,
|
|
63
|
+
14,
|
|
64
|
+
15,
|
|
65
|
+
16
|
|
66
|
+
]) others.push(await order(`./?seed=${seed}${everything}`));
|
|
67
|
+
expect(others.some((other) => other !== first)).toBe(true);
|
|
68
|
+
}
|
|
69
|
+
await page.goto("./");
|
|
70
|
+
await expect.poll(() => new URL(page.url()).searchParams.get("seed")).toMatch(/^\d+$/);
|
|
71
|
+
});
|
|
72
|
+
test("all right answers give the full score, then the solutions show", async ({ page }) => {
|
|
73
|
+
await page.goto("./");
|
|
74
|
+
if (quiz.questions.length > 1) await page.locator(".quiz-start button").click();
|
|
75
|
+
expect(await page.evaluate(() => Object.keys(localStorage))).toEqual([]);
|
|
76
|
+
for (let step = 0; step < quiz.questions.length; step++) {
|
|
77
|
+
const id = await page.locator(".question:visible").getAttribute("data-id");
|
|
78
|
+
const item = quiz.questions.find((candidate) => candidate.id === id);
|
|
79
|
+
if (item === void 0) throw new Error(`no visible question at step ${step + 1}`);
|
|
80
|
+
await answerQuestion(page, item, rightAnswer(item));
|
|
81
|
+
await question(page, item).locator(".question-next").click();
|
|
82
|
+
}
|
|
83
|
+
const total = String(quiz.questions.length);
|
|
84
|
+
await expect(page.locator(".quiz-score")).toHaveText(de.score.replace("{score}", total).replace("{total}", total));
|
|
85
|
+
await expect(page.locator("#loesungen")).toBeVisible();
|
|
86
|
+
const stored = await page.evaluate(() => Object.keys(localStorage));
|
|
87
|
+
if (quiz.rememberLastResult === true) {
|
|
88
|
+
expect(stored).toHaveLength(1);
|
|
89
|
+
expect(stored[0]).toMatch(/^lernapps:.+:last-result$/);
|
|
90
|
+
} else expect(stored).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
if (quiz.questions.length > 1) test("the learner chooses how many questions to answer; the address keeps the choice", async ({ page }) => {
|
|
93
|
+
await page.goto("./?seed=11");
|
|
94
|
+
await expect(page.locator(".quiz-start")).toBeVisible();
|
|
95
|
+
await expect(page.locator(".question:visible")).toHaveCount(0);
|
|
96
|
+
const count = page.locator("#quiz-count");
|
|
97
|
+
await expect(count).toHaveValue(String(quiz.questions.length));
|
|
98
|
+
await expect(count).toHaveAttribute("max", String(quiz.questions.length));
|
|
99
|
+
await count.fill("1");
|
|
100
|
+
await page.locator(".quiz-start button").click();
|
|
101
|
+
await expect(page.locator(".quiz-start")).toBeHidden();
|
|
102
|
+
expect(new URL(page.url()).searchParams.get("n")).toBe("1");
|
|
103
|
+
await expect(page.locator(".question")).toHaveCount(1);
|
|
104
|
+
await expect(page.locator(".quiz-solution")).toHaveCount(1);
|
|
105
|
+
const id = await page.locator(".question:visible").getAttribute("data-id");
|
|
106
|
+
const item = quiz.questions.find((candidate) => candidate.id === id);
|
|
107
|
+
if (item === void 0) throw new Error("no visible question");
|
|
108
|
+
await answerQuestion(page, item, rightAnswer(item));
|
|
109
|
+
await question(page, item).locator(".question-next").click();
|
|
110
|
+
await expect(page.locator(".quiz-score")).toHaveText(de.score.replace("{score}", "1").replace("{total}", "1"));
|
|
111
|
+
await page.goto(page.url());
|
|
112
|
+
await expect(page.locator(".quiz-start")).toBeHidden();
|
|
113
|
+
await expect(page.locator(".question")).toHaveCount(1);
|
|
114
|
+
await expect(page.locator(".question")).toHaveAttribute("data-id", item.id);
|
|
115
|
+
});
|
|
116
|
+
test.describe("without JavaScript", () => {
|
|
117
|
+
test.use({ javaScriptEnabled: false });
|
|
118
|
+
test("every question and its solution can be read", async ({ page }) => {
|
|
119
|
+
await page.goto("./");
|
|
120
|
+
for (const item of quiz.questions) {
|
|
121
|
+
await expect(question(page, item).locator("legend")).toHaveText(item.text);
|
|
122
|
+
await expectMaterial(page, item, { loaded: false });
|
|
123
|
+
await expectBackground(page.locator(`#loesung-${item.id}`), item);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
for (const lang of languages.slice(1)) {
|
|
128
|
+
const local = quizIn(all, lang);
|
|
129
|
+
const words = messagesOf(lang);
|
|
130
|
+
const address = `./${lang}/`;
|
|
131
|
+
test.describe(`in ${lang}`, () => {
|
|
132
|
+
test.describe("without JavaScript", () => {
|
|
133
|
+
test.use({ javaScriptEnabled: false });
|
|
134
|
+
test(`the page is in ${lang}, every question and its solution can be read, the switch leads to every language`, async ({ page }) => {
|
|
135
|
+
await page.goto(address);
|
|
136
|
+
await expect(page.locator("html")).toHaveAttribute("lang", lang);
|
|
137
|
+
await expect(page.locator("h1")).toHaveText(local.title);
|
|
138
|
+
await expect(page.locator("#loesungen h2")).toHaveText(words.solutions);
|
|
139
|
+
for (const item of local.questions) {
|
|
140
|
+
await expect(question(page, item).locator("legend")).toHaveText(item.text);
|
|
141
|
+
await expectMaterial(page, item, { loaded: false });
|
|
142
|
+
await expectBackground(page.locator(`#loesung-${item.id}`), item);
|
|
143
|
+
}
|
|
144
|
+
for (const other of languages) await expect(page.locator(`link[rel="alternate"][hreflang="${other}"]`)).toHaveCount(1);
|
|
145
|
+
for (const other of languages.filter((candidate) => candidate !== lang)) {
|
|
146
|
+
await page.goto(address);
|
|
147
|
+
await page.locator(`.page-languages a[hreflang="${other}"]`).click();
|
|
148
|
+
await expect(page.locator("html")).toHaveAttribute("lang", other);
|
|
149
|
+
await expect(page.locator("h1")).toHaveText(quizIn(all, other).title);
|
|
150
|
+
await expect(page.locator(".page-languages a[aria-current=\"page\"]")).toHaveAttribute("hreflang", other);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
test(`what a question shows, in ${lang}`, async ({ page }) => {
|
|
155
|
+
const item = local.questions.find((candidate) => candidate.material !== void 0);
|
|
156
|
+
test.skip(item === void 0, "no question shows content");
|
|
157
|
+
if (item === void 0) return;
|
|
158
|
+
await page.goto(`${address}#${questionId(item)}`);
|
|
159
|
+
await expectMaterial(page, item);
|
|
160
|
+
});
|
|
161
|
+
test(`one question, answered in ${lang}`, async ({ page }) => {
|
|
162
|
+
const item = local.questions[0];
|
|
163
|
+
if (item === void 0) throw new Error("the quiz has no questions");
|
|
164
|
+
await page.goto(`${address}#${questionId(item)}`);
|
|
165
|
+
await expect(question(page, item)).toBeVisible();
|
|
166
|
+
await answerQuestion(page, item, rightAnswer(item), lang);
|
|
167
|
+
await expect(question(page, item).locator(".question-feedback")).toContainText(words.right);
|
|
168
|
+
await expectBackground(question(page, item).locator(".question-reveal"), item);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export { answerQuestion, expectBackground, expectFeedbackInView, expectMaterial, quizTests };
|