@lernapps/app-templates 0.0.1 → 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/LICENSE +22 -0
- package/README.md +195 -2
- package/dist/appname-Bh23cUuz.mjs +12 -0
- package/dist/assessment-Dy359O1O.mjs +1696 -0
- package/dist/block-BK_ohsH1.mjs +82 -0
- package/dist/blocks-CxtCPAa5.mjs +149 -0
- package/dist/blocks.mjs +4 -0
- package/dist/check-CteIFN96.mjs +254 -0
- package/dist/content-DO6oEJxj.mjs +140 -0
- package/dist/contract-PONpxJtd.mjs +52 -0
- package/dist/e2e-CU0n_unV.mjs +89 -0
- package/dist/engine-CArxqizW.mjs +136 -0
- package/dist/explainer-e2e.mjs +1311 -0
- package/dist/explainer-islands.mjs +197 -0
- package/dist/explainer-plugin.mjs +2132 -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-RosvaPZf.mjs +16976 -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-DnW-xtCh.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-D5kwkNt1.mjs +507 -0
- package/dist/render-DqHOJyHK.mjs +146 -0
- package/dist/render-GS1Htlu8.mjs +545 -0
- package/dist/result-D_mmCVwN.mjs +180 -0
- package/dist/tasks-DADUYy52.mjs +74 -0
- package/dist/validate-BDwL7Su3.mjs +1076 -0
- package/dist/validate-Bxy9NfB8.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 +143 -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 +448 -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 +1488 -0
- package/runtime/explainer/exercises.ts +326 -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 +306 -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 +433 -0
- package/runtime/shared/questions/question.v1.schema.json +346 -0
- package/runtime/shared/questions/questions.css +192 -0
- package/runtime/shared/questions/render.ts +255 -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/speech.ts +39 -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,686 @@
|
|
|
1
|
+
// Renders every page of an explainer at build time into static HTML: the text, every formula as MathML, every figure
|
|
2
|
+
// as inline SVG or an image, tables, timelines, definitions, quotes, highlighted code, the exercises (exercises.ts:
|
|
3
|
+
// each with its solution in a <details>, a generated one with three worked tasks), and the navigation (contents,
|
|
4
|
+
// breadcrumbs, previous and next, the contents of a page with three or more sections, the glossary, the language
|
|
5
|
+
// switch). Without JavaScript the page is complete; the client module only adds. A vue-component block is its island:
|
|
6
|
+
// the component's markup as the build rendered it (`hidden` until it hydrates), its nojs text, its props in the page's
|
|
7
|
+
// language, and for an exercise its anchor, its task number with "Neue Aufgabe", the task of number 1 as its describe
|
|
8
|
+
// states it with the solution folded, and the place of its feedback. Links to other sites are plain links with rel="noopener":
|
|
9
|
+
// nothing is loaded from another server before a click; a video is such a link, with its player only after a click
|
|
10
|
+
// (video.ts). The end-to-end tests rely on the classes, ids and rel
|
|
11
|
+
// attributes written here.
|
|
12
|
+
import { questionMessagesIn } from "../shared/questions/render.ts";
|
|
13
|
+
import { propsIn } from "./components/props.ts";
|
|
14
|
+
import {
|
|
15
|
+
inLanguage,
|
|
16
|
+
termId as termIdOf,
|
|
17
|
+
timelineEvents,
|
|
18
|
+
type Block,
|
|
19
|
+
type BlockOf,
|
|
20
|
+
type Event,
|
|
21
|
+
type Site,
|
|
22
|
+
type Text,
|
|
23
|
+
type Descriptions,
|
|
24
|
+
} from "./content.ts";
|
|
25
|
+
import { renderExercise } from "./exercises.ts";
|
|
26
|
+
import { isSizeless } from "../shared/blocks/image.ts";
|
|
27
|
+
import { renderMaterial, type MaterialContext } from "../shared/blocks/index.ts";
|
|
28
|
+
import { localeOf } from "../shared/languages.ts";
|
|
29
|
+
import { plain, renderInline, renderMarkdown } from "../shared/text/markdown.ts";
|
|
30
|
+
import { message, type Messages } from "./messages.ts";
|
|
31
|
+
import { alternateLinks, languageRoot, pageFooter, pageHeader, relative } from "../shared/pages.ts";
|
|
32
|
+
import { contentPages, hiddenChapter, routes, terms, type Route } from "./routes.ts";
|
|
33
|
+
import { tutorPageOf } from "./tutor.ts";
|
|
34
|
+
import { LANGUAGES, languageName, NEEDS, OTHER_LANGUAGE, type Need } from "./needs.ts";
|
|
35
|
+
import { BASE, placeholderPrompt, plainAddress, PROVIDERS, QUERY, startPrompt } from "./prompt.ts";
|
|
36
|
+
import { pageExercises, sectionsOf, TUTOR_MARKDOWN } from "./tutorpage.ts";
|
|
37
|
+
import { NEW_TASK_ICON } from "./tasks.ts";
|
|
38
|
+
import { renderVideo } from "./video.ts";
|
|
39
|
+
import { assessmentOf } from "./assessment.ts";
|
|
40
|
+
import { assessmentMain, assessmentOffer, type AssessmentContext } from "./assessment-page.ts";
|
|
41
|
+
|
|
42
|
+
function sizeless(site: Site, image: string): boolean {
|
|
43
|
+
const file = site.images.get(image);
|
|
44
|
+
return file !== undefined && isSizeless(file);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A rendered page: its language, what goes into <head>, and what replaces the marker in <body>. */
|
|
48
|
+
export interface RenderedPage {
|
|
49
|
+
route: Route;
|
|
50
|
+
head: string;
|
|
51
|
+
body: string;
|
|
52
|
+
/** Whether it has a component, which the islands module hydrates. */
|
|
53
|
+
islands: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The markup of each vue-component block in each language, as the build rendered it (plugin.ts). */
|
|
57
|
+
export type Islands = ReadonlyMap<Block, ReadonlyMap<string, string>>;
|
|
58
|
+
|
|
59
|
+
/** What rendering one page needs. */
|
|
60
|
+
class Context {
|
|
61
|
+
readonly locale: string;
|
|
62
|
+
readonly site: Site;
|
|
63
|
+
readonly route: Route;
|
|
64
|
+
readonly islands: Islands;
|
|
65
|
+
readonly descriptions: Descriptions;
|
|
66
|
+
/** What the blocks that show content need from this page. */
|
|
67
|
+
readonly material: MaterialContext;
|
|
68
|
+
/** Whether a component was rendered. */
|
|
69
|
+
hasIslands = false;
|
|
70
|
+
constructor(site: Site, route: Route, islands: Islands, descriptions: Descriptions) {
|
|
71
|
+
this.site = site;
|
|
72
|
+
this.route = route;
|
|
73
|
+
this.islands = islands;
|
|
74
|
+
this.descriptions = descriptions;
|
|
75
|
+
this.locale = localeOf(route.lang);
|
|
76
|
+
this.material = {
|
|
77
|
+
lang: route.lang,
|
|
78
|
+
href: (image) => relative(route.path, image),
|
|
79
|
+
resolve: this.resolve,
|
|
80
|
+
sizeless: (image) => sizeless(site, image),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
get lang(): string {
|
|
84
|
+
return this.route.lang;
|
|
85
|
+
}
|
|
86
|
+
t(key: keyof Messages, params?: Readonly<Record<string, string>>): string {
|
|
87
|
+
return message(this.lang, key, params);
|
|
88
|
+
}
|
|
89
|
+
/** The text in this page's language, as plain HTML. */
|
|
90
|
+
text(text: Text): string {
|
|
91
|
+
return plain(inLanguage(text, this.lang));
|
|
92
|
+
}
|
|
93
|
+
/** A link from this page to `local` (an address within the language tree, e.g. "prozent/prozentwert/#regel"). */
|
|
94
|
+
to(local: string): string {
|
|
95
|
+
return relative(this.route.path, languageRoot(this.lang) + local);
|
|
96
|
+
}
|
|
97
|
+
/** Where a link inside a text leads: /<chapter>/<page>/#<section> within this language's tree. */
|
|
98
|
+
readonly resolve = (href: string): string => this.to(href.replace(/^\//, ""));
|
|
99
|
+
markdown(text: Text): string {
|
|
100
|
+
return renderMarkdown(inLanguage(text, this.lang), this.resolve);
|
|
101
|
+
}
|
|
102
|
+
inline(text: Text): string {
|
|
103
|
+
return renderInline(inLanguage(text, this.lang), this.resolve);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const external = (url: string, text = url) => `<a href="${plain(url)}" rel="noopener">${plain(text)}</a>`;
|
|
108
|
+
|
|
109
|
+
function dateLabel(context: Context, event: Event): { label: string; datetime?: string } {
|
|
110
|
+
const date = String(event.date);
|
|
111
|
+
const match = /^(-?\d{1,4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(date);
|
|
112
|
+
const year = Number(match?.[1] ?? 0);
|
|
113
|
+
const datetime = year >= 0 && match !== null ? date.replace(/^\d+/, (digits) => digits.padStart(4, "0")) : undefined;
|
|
114
|
+
if (event.label !== undefined)
|
|
115
|
+
return { label: context.text(event.label), ...(datetime === undefined ? {} : { datetime }) };
|
|
116
|
+
if (year < 0) return { label: plain(context.t("beforeChrist", { year: String(-year) })) };
|
|
117
|
+
let label = String(year);
|
|
118
|
+
if (match?.[2] !== undefined) {
|
|
119
|
+
const when = new Date(Date.UTC(year, Number(match[2]) - 1, Number(match[3] ?? 1)));
|
|
120
|
+
when.setUTCFullYear(year);
|
|
121
|
+
label = new Intl.DateTimeFormat(context.locale, {
|
|
122
|
+
timeZone: "UTC",
|
|
123
|
+
year: "numeric",
|
|
124
|
+
month: "long",
|
|
125
|
+
...(match[3] === undefined ? {} : { day: "numeric" }),
|
|
126
|
+
}).format(when);
|
|
127
|
+
}
|
|
128
|
+
return { label: plain(label), ...(datetime === undefined ? {} : { datetime }) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderEvents(context: Context, events: readonly Event[]): string {
|
|
132
|
+
const items = events.map((event) => {
|
|
133
|
+
const { label, datetime } = dateLabel(context, event);
|
|
134
|
+
const when =
|
|
135
|
+
datetime === undefined
|
|
136
|
+
? `<span class="explainer-date">${label}</span>`
|
|
137
|
+
: `<time class="explainer-date" datetime="${datetime}">${label}</time>`;
|
|
138
|
+
const text =
|
|
139
|
+
event.text === undefined ? "" : `<div class="explainer-event-text">${context.markdown(event.text)}</div>`;
|
|
140
|
+
return `<li><p class="explainer-event">${when} <strong>${context.text(event.title)}</strong></p>${text}</li>`;
|
|
141
|
+
});
|
|
142
|
+
return `<ol class="explainer-timeline">${items.join("")}</ol>`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The blocks, at heading level `level` (the level of the heading they are under: 1 for the page). */
|
|
146
|
+
function renderBlocks(context: Context, blocks: readonly Block[], level: number, anchor: string[] = []): string {
|
|
147
|
+
return blocks.map((block) => renderBlock(context, block, level, anchor)).join("\n");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function renderBlock(context: Context, block: Block, level: number, anchor: string[]): string {
|
|
151
|
+
switch (block.type) {
|
|
152
|
+
case "section": {
|
|
153
|
+
const id = [...anchor, block.id];
|
|
154
|
+
return [
|
|
155
|
+
`<section class="explainer-section">`,
|
|
156
|
+
`<h${level + 1} id="${id.join("/")}">${context.text(block.title)}</h${level + 1}>`,
|
|
157
|
+
renderBlocks(context, block.blocks, level + 1, id),
|
|
158
|
+
`</section>`,
|
|
159
|
+
].join("\n");
|
|
160
|
+
}
|
|
161
|
+
case "example":
|
|
162
|
+
case "note":
|
|
163
|
+
return [
|
|
164
|
+
`<div class="explainer-box explainer-${block.type}">`,
|
|
165
|
+
`<h${level + 1} class="explainer-box-title">${context.text(block.title)}</h${level + 1}>`,
|
|
166
|
+
renderBlocks(context, block.blocks, level + 1, anchor),
|
|
167
|
+
`</div>`,
|
|
168
|
+
].join("\n");
|
|
169
|
+
case "text":
|
|
170
|
+
case "formula":
|
|
171
|
+
case "figure":
|
|
172
|
+
case "labelled-figure":
|
|
173
|
+
case "table":
|
|
174
|
+
return renderMaterial([block], context.material);
|
|
175
|
+
case "timeline":
|
|
176
|
+
return renderEvents(context, timelineEvents(context.site, block));
|
|
177
|
+
case "definition": {
|
|
178
|
+
const id = termIdOf(block.term);
|
|
179
|
+
return [
|
|
180
|
+
`<dl class="explainer-definition">`,
|
|
181
|
+
`<dt><dfn id="begriff-${id}"><a href="${context.to(`glossar/#${id}`)}">${context.text(block.term)}</a></dfn></dt>`,
|
|
182
|
+
`<dd>${context.markdown(block.explanation)}</dd>`,
|
|
183
|
+
`</dl>`,
|
|
184
|
+
].join("");
|
|
185
|
+
}
|
|
186
|
+
case "quote":
|
|
187
|
+
case "code":
|
|
188
|
+
return renderMaterial([block], context.material);
|
|
189
|
+
case "video":
|
|
190
|
+
return renderVideo(block, context.lang, context.site.storage ?? "explainer");
|
|
191
|
+
case "vue-component":
|
|
192
|
+
return renderComponent(context, block);
|
|
193
|
+
default:
|
|
194
|
+
return renderExercise(
|
|
195
|
+
block,
|
|
196
|
+
context.lang,
|
|
197
|
+
context.site.data,
|
|
198
|
+
context.resolve,
|
|
199
|
+
context.material,
|
|
200
|
+
context.site.storage ?? "explainer",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const json = (value: unknown) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A vue-component block: its island, hidden until it hydrates, its nojs text, its props and its feedback. An exercise
|
|
209
|
+
* component has its anchor, its task number with "Neue Aufgabe" (hidden until it hydrates), its island in a form with
|
|
210
|
+
* the explainer's "Prüfen" (the row of the shared questions, hidden until it hydrates; Enter in a field of the
|
|
211
|
+
* component checks too), and as its no-JS text the task of number 1 as its describe states it, with the solution
|
|
212
|
+
* folded, under the block's own nojs text if it has one.
|
|
213
|
+
*/
|
|
214
|
+
function renderComponent(context: Context, block: BlockOf<"vue-component">): string {
|
|
215
|
+
const use = context.site.components.find((candidate) => candidate.block === block);
|
|
216
|
+
if (use === undefined) return "";
|
|
217
|
+
context.hasIslands = true;
|
|
218
|
+
const { component } = use;
|
|
219
|
+
const markup = context.islands.get(block)?.get(context.lang) ?? "";
|
|
220
|
+
const messages = questionMessagesIn(context.lang);
|
|
221
|
+
const feedback = component.exercise
|
|
222
|
+
? `<p class="question-feedback" data-right="${plain(messages.right)}" data-wrong="${plain(messages.wrong)}"></p>`
|
|
223
|
+
: "";
|
|
224
|
+
const first = context.descriptions.get(block)?.tasks[context.lang]?.[0];
|
|
225
|
+
const number = (seed: number) => plain(context.t("taskNumber", { n: String(seed) }));
|
|
226
|
+
const described =
|
|
227
|
+
first === undefined
|
|
228
|
+
? ""
|
|
229
|
+
: [
|
|
230
|
+
`<div class="explainer-described">`,
|
|
231
|
+
`<p class="explainer-task-number">${number(first.number)}</p>`,
|
|
232
|
+
`<div class="explainer-task">${renderMarkdown(first.task, context.resolve)}</div>`,
|
|
233
|
+
`<details class="explainer-solution"><summary>${plain(messages.solution)}</summary>${renderMarkdown(first.solution, context.resolve)}</details>`,
|
|
234
|
+
`</div>`,
|
|
235
|
+
].join("");
|
|
236
|
+
const nojs = (block.nojs === undefined ? "" : context.markdown(block.nojs)) + described;
|
|
237
|
+
const controls = component.exercise
|
|
238
|
+
? `<p class="explainer-task-controls" hidden><span class="explainer-task-number" tabindex="-1" data-template="${plain(context.t("taskNumber"))}">${number(1)}</span> <button type="button" class="explainer-new-task" aria-label="${plain(context.t("newTask"))}" title="${plain(context.t("newTask"))}">${NEW_TASK_ICON}</button></p>`
|
|
239
|
+
: "";
|
|
240
|
+
const attributes = [
|
|
241
|
+
...(use.id === undefined ? [] : [`id="${use.id}"`]),
|
|
242
|
+
`data-component="${component.name}"`,
|
|
243
|
+
// the seed of each task number, which the islands draw the task from
|
|
244
|
+
...(component.exercise
|
|
245
|
+
? [`data-exercise data-seeds="${JSON.stringify(context.descriptions.get(block)?.seeds ?? [1])}"`]
|
|
246
|
+
: []),
|
|
247
|
+
// it declares the prop checked, which the islands set once "Prüfen" judged its answer
|
|
248
|
+
...(component.checked ? ["data-checked"] : []),
|
|
249
|
+
...(component.caption ? [`data-nojs="caption" data-caption="${plain(context.t("textDescription"))}"`] : []),
|
|
250
|
+
];
|
|
251
|
+
return [
|
|
252
|
+
`<div class="explainer-component" ${attributes.join(" ")}>`,
|
|
253
|
+
controls,
|
|
254
|
+
component.exercise
|
|
255
|
+
? `<form class="explainer-component-form" novalidate><div class="explainer-island" hidden>${markup}</div><p class="question-actions" hidden><button type="submit" class="question-check" disabled>${plain(messages.check)}</button></p></form>`
|
|
256
|
+
: `<div class="explainer-island" hidden>${markup}</div>`,
|
|
257
|
+
`<div class="explainer-nojs">${nojs}</div>`,
|
|
258
|
+
feedback,
|
|
259
|
+
`<script type="application/json" class="explainer-component-props">${json(propsIn(block.props, component, context.lang))}</script>`,
|
|
260
|
+
`</div>`,
|
|
261
|
+
]
|
|
262
|
+
.filter((part) => part !== "")
|
|
263
|
+
.join("\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The contents of a page with three or more sections: the sections and their subsections. */
|
|
267
|
+
function pageContents(context: Context, blocks: readonly Block[]): string {
|
|
268
|
+
const sections = blocks.filter((block) => block.type === "section");
|
|
269
|
+
if (sections.length < 3) return "";
|
|
270
|
+
const list = (items: readonly Block[], anchor: string[]): string =>
|
|
271
|
+
`<ol>${items
|
|
272
|
+
.filter((block) => block.type === "section")
|
|
273
|
+
.map((section) => {
|
|
274
|
+
const id = [...anchor, section.id];
|
|
275
|
+
const inner = section.blocks.some((block) => block.type === "section") ? list(section.blocks, id) : "";
|
|
276
|
+
return `<li><a href="#${id.join("/")}">${context.text(section.title)}</a>${inner}</li>`;
|
|
277
|
+
})
|
|
278
|
+
.join("")}</ol>`;
|
|
279
|
+
return `<nav class="explainer-toc" aria-label="${plain(context.t("onThisPage"))}"><p class="explainer-toc-title">${plain(context.t("onThisPage"))}</p>${list(blocks, [])}</nav>`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function header(context: Context): string {
|
|
283
|
+
const { site, route } = context;
|
|
284
|
+
// only the language switch: the breadcrumbs lead to the start page, and the way back to the catalog is the platform's
|
|
285
|
+
return pageHeader({
|
|
286
|
+
...route,
|
|
287
|
+
languages: site.app.languages,
|
|
288
|
+
label: plain(context.t("languages")),
|
|
289
|
+
name: (lang) => plain(message(lang, "language")),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function breadcrumbs(context: Context): string {
|
|
294
|
+
const { route, site } = context;
|
|
295
|
+
if (route.kind === "start") return "";
|
|
296
|
+
const items = [`<li><a href="${context.to("")}">${plain(context.t("start"))}</a></li>`];
|
|
297
|
+
if ((route.kind === "page" || route.kind === "assessment") && route.chapter !== undefined && !hiddenChapter(site)) {
|
|
298
|
+
items.push(`<li><a href="${context.to(`${route.chapter.id}/`)}">${context.text(route.chapter.title)}</a></li>`);
|
|
299
|
+
}
|
|
300
|
+
items.push(`<li aria-current="page">${plain(route.title)}</li>`);
|
|
301
|
+
return `<nav class="explainer-breadcrumbs" aria-label="${plain(context.t("breadcrumbs"))}"><ol>${items.join("")}</ol></nav>`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function pager(context: Context): string {
|
|
305
|
+
const { route, site } = context;
|
|
306
|
+
if (route.page === undefined) return "";
|
|
307
|
+
const pages = contentPages(site);
|
|
308
|
+
const index = pages.indexOf(route.page);
|
|
309
|
+
const link = (rel: "prev" | "next", key: "previous" | "next", offset: number) => {
|
|
310
|
+
const other = pages[index + offset];
|
|
311
|
+
if (other === undefined) return "";
|
|
312
|
+
const title = inLanguage(other.title, context.lang);
|
|
313
|
+
return `<a rel="${rel}" href="${context.to(`${other.chapter}/${other.id}/`)}">${plain(context.t(key, { title }))}</a>`;
|
|
314
|
+
};
|
|
315
|
+
return `<nav class="explainer-pager" aria-label="${plain(context.t("pager"))}">${link("prev", "previous", -1)}${link("next", "next", 1)}</nav>`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function pageList(context: Context, pages: readonly NonNullable<Route["page"]>[]): string {
|
|
319
|
+
return `<ol class="explainer-pages">${pages
|
|
320
|
+
.map((page) => {
|
|
321
|
+
const description =
|
|
322
|
+
page.description === undefined
|
|
323
|
+
? ""
|
|
324
|
+
: ` <span class="explainer-description">${context.text(page.description)}</span>`;
|
|
325
|
+
return `<li><a href="${context.to(`${page.chapter}/${page.id}/`)}">${context.text(page.title)}</a>${description}</li>`;
|
|
326
|
+
})
|
|
327
|
+
.join("")}</ol>`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function main(context: Context): string {
|
|
331
|
+
const { route, site } = context;
|
|
332
|
+
const title = `<h1>${plain(route.title)}</h1>`;
|
|
333
|
+
switch (route.kind) {
|
|
334
|
+
case "start": {
|
|
335
|
+
const contents = hiddenChapter(site)
|
|
336
|
+
? pageList(context, site.chapters[0]?.pages ?? [])
|
|
337
|
+
: `<ol class="explainer-chapters">${site.chapters
|
|
338
|
+
.map((chapter) => {
|
|
339
|
+
const summary = chapter.summary === undefined ? "" : context.markdown(chapter.summary);
|
|
340
|
+
return `<li><a class="explainer-chapter-link" href="${context.to(`${chapter.id}/`)}">${context.text(chapter.title)}</a>${summary}${pageList(context, chapter.pages)}</li>`;
|
|
341
|
+
})
|
|
342
|
+
.join("")}</ol>`;
|
|
343
|
+
const glossary =
|
|
344
|
+
terms(site).length === 0
|
|
345
|
+
? ""
|
|
346
|
+
: `<p><a href="${context.to("glossar/")}">${plain(context.t("glossary"))}</a></p>`;
|
|
347
|
+
const sources =
|
|
348
|
+
site.app.sources === undefined
|
|
349
|
+
? ""
|
|
350
|
+
: `<section class="explainer-sources"><h2>${plain(context.t("sources"))}</h2><p>${plain(context.t("sourcesIntro"))}</p><ul>${site.app.sources
|
|
351
|
+
.map((source) => {
|
|
352
|
+
const text = inLanguage(source.title, context.lang);
|
|
353
|
+
return `<li>${source.url === undefined ? plain(text) : external(source.url, text)}</li>`;
|
|
354
|
+
})
|
|
355
|
+
.join("")}</ul></section>`;
|
|
356
|
+
const assessment = assessmentOf(site);
|
|
357
|
+
return [
|
|
358
|
+
title,
|
|
359
|
+
`<div class="explainer-intro">${context.markdown(site.app.intro)}</div>`,
|
|
360
|
+
assessment === undefined ? "" : assessmentOffer(assessmentContext(context), assessment),
|
|
361
|
+
`<nav class="explainer-contents" aria-labelledby="inhalt"><h2 id="inhalt">${plain(context.t("contents"))}</h2>${contents}</nav>`,
|
|
362
|
+
glossary,
|
|
363
|
+
tutorAbout(context),
|
|
364
|
+
sources,
|
|
365
|
+
]
|
|
366
|
+
.filter((part) => part !== "")
|
|
367
|
+
.join("\n");
|
|
368
|
+
}
|
|
369
|
+
case "chapter": {
|
|
370
|
+
const chapter = route.chapter;
|
|
371
|
+
if (chapter === undefined) return title;
|
|
372
|
+
const summary =
|
|
373
|
+
chapter.summary === undefined ? "" : `<div class="explainer-intro">${context.markdown(chapter.summary)}</div>`;
|
|
374
|
+
const assessment = assessmentOf(site, chapter.id);
|
|
375
|
+
return [
|
|
376
|
+
title,
|
|
377
|
+
summary,
|
|
378
|
+
assessment === undefined ? "" : assessmentOffer(assessmentContext(context), assessment),
|
|
379
|
+
`<nav class="explainer-contents" aria-label="${plain(context.t("pages"))}">${pageList(context, chapter.pages)}</nav>`,
|
|
380
|
+
]
|
|
381
|
+
.filter((part) => part !== "")
|
|
382
|
+
.join("\n");
|
|
383
|
+
}
|
|
384
|
+
case "assessment":
|
|
385
|
+
return route.assessment === undefined ? title : assessmentMain(assessmentContext(context), route.assessment);
|
|
386
|
+
case "glossary": {
|
|
387
|
+
const collator = new Intl.Collator(context.locale);
|
|
388
|
+
const entries = terms(site)
|
|
389
|
+
.sort((a, b) => collator.compare(inLanguage(a.term, context.lang), inLanguage(b.term, context.lang)))
|
|
390
|
+
.map((term) => {
|
|
391
|
+
const page = inLanguage(term.page.title, context.lang);
|
|
392
|
+
const back = `<a href="${context.to(`${term.page.chapter}/${term.page.id}/#begriff-${term.id}`)}">${plain(context.t("explainedOn", { page }))}</a>`;
|
|
393
|
+
return `<dt id="${term.id}"><dfn>${context.text(term.term)}</dfn></dt><dd>${context.markdown(term.explanation)}<p>${back}</p></dd>`;
|
|
394
|
+
});
|
|
395
|
+
return [
|
|
396
|
+
title,
|
|
397
|
+
`<p>${plain(context.t("glossaryIntro"))}</p>`,
|
|
398
|
+
`<dl class="explainer-glossary">${entries.join("")}</dl>`,
|
|
399
|
+
].join("\n");
|
|
400
|
+
}
|
|
401
|
+
case "page": {
|
|
402
|
+
const page = route.page;
|
|
403
|
+
if (page === undefined) return title;
|
|
404
|
+
// the title and way A in one row: beside it on a wide screen, below it on a narrow one (explainer.css)
|
|
405
|
+
return [
|
|
406
|
+
`<div class="explainer-title-row">`,
|
|
407
|
+
title,
|
|
408
|
+
tutorSection(context),
|
|
409
|
+
`</div>`,
|
|
410
|
+
pageContents(context, page.blocks),
|
|
411
|
+
renderBlocks(context, page.blocks, 1),
|
|
412
|
+
].join("\n");
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** What a part of a self-assessment needs of the page it is on (assessment-page.ts). */
|
|
418
|
+
const assessmentContext = (context: Context): AssessmentContext => ({
|
|
419
|
+
site: context.site,
|
|
420
|
+
lang: context.lang,
|
|
421
|
+
to: (local) => context.to(local),
|
|
422
|
+
resolve: context.resolve,
|
|
423
|
+
material: context.material,
|
|
424
|
+
ways: wayOptions(context),
|
|
425
|
+
component: (block, key) => assessmentComponent(context, block, key),
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* An exercise component as one exercise of a self-assessment, by its key: its island with the task of number 1 as the
|
|
430
|
+
* build rendered it, its props and its task numbers, without "Prüfen", feedback, task number or nojs text (the
|
|
431
|
+
* self-assessment runs with JavaScript only); the islands hydrate it with the task the self-assessment draws.
|
|
432
|
+
*/
|
|
433
|
+
function assessmentComponent(
|
|
434
|
+
context: Context,
|
|
435
|
+
block: BlockOf<"vue-component">,
|
|
436
|
+
key: string,
|
|
437
|
+
): { html: string; seeds: number[] } | undefined {
|
|
438
|
+
const use = context.site.components.find((candidate) => candidate.block === block);
|
|
439
|
+
if (use === undefined || !use.component.exercise) return undefined;
|
|
440
|
+
context.hasIslands = true;
|
|
441
|
+
const { component } = use;
|
|
442
|
+
const seeds = context.descriptions.get(block)?.seeds ?? [1];
|
|
443
|
+
const attributes = [
|
|
444
|
+
`id="aufgabe-${key}"`,
|
|
445
|
+
`data-component="${component.name}"`,
|
|
446
|
+
`data-exercise data-assessment data-seeds="${JSON.stringify(seeds)}"`,
|
|
447
|
+
...(component.checked ? ["data-checked"] : []),
|
|
448
|
+
];
|
|
449
|
+
const html = [
|
|
450
|
+
`<div class="explainer-component" ${attributes.join(" ")}>`,
|
|
451
|
+
`<div class="explainer-island" hidden>${context.islands.get(block)?.get(context.lang) ?? ""}</div>`,
|
|
452
|
+
`<script type="application/json" class="explainer-component-props">${json(propsIn(block.props, component, context.lang))}</script>`,
|
|
453
|
+
`</div>`,
|
|
454
|
+
].join("\n");
|
|
455
|
+
return { html, seeds: [...seeds] };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* What a KI tutor is, on the start page: a section of its own that says what the tutor does and where it shows, the
|
|
460
|
+
* choice of the learner's assistant ("Keiner" first; shown by the client module, which keeps it on the device), that the
|
|
461
|
+
* learner uses an account of their own and the assistant is no offer of lernapps.net, and that nothing leaves the app
|
|
462
|
+
* before a click. It starts no tutor itself.
|
|
463
|
+
*/
|
|
464
|
+
function tutorAbout(context: Context): string {
|
|
465
|
+
const { site } = context;
|
|
466
|
+
return [
|
|
467
|
+
`<section class="explainer-tutor-about" aria-labelledby="ki-tutor">`,
|
|
468
|
+
`<h2 id="ki-tutor">${plain(context.t("tutorTitle"))}</h2>`,
|
|
469
|
+
`<p>${plain(context.t("tutorAbout"))}</p>`,
|
|
470
|
+
`<p>${plain(context.t("tutorAboutStart"))}</p>`,
|
|
471
|
+
`<p class="explainer-tutor-choice" hidden><label>${plain(context.t("tutorChoose"))} <select class="explainer-tutor-choose" data-storage="${plain(site.storage ?? "explainer")}"><option value="">${plain(context.t("tutorNone"))}</option>${wayOptions(context)}</select></label> <button type="button" class="explainer-tutor-needs-open" aria-haspopup="dialog" aria-label="${plain(context.t("needsOpen"))}" title="${plain(context.t("needsOpen"))}">${SETTINGS_ICON}</button></p>`,
|
|
472
|
+
`<p class="explainer-tutor-hint" role="status" aria-live="polite" hidden></p>`,
|
|
473
|
+
`<p>${plain(context.t("tutorAccount"))}</p>`,
|
|
474
|
+
`<p>${plain(context.t("tutorAboutPrivacy"))}</p>`,
|
|
475
|
+
`</section>`,
|
|
476
|
+
needsChoice(context),
|
|
477
|
+
].join("\n");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** The symbol of "Einstellungen", which opens the dialog of the learner's needs: a gear, its name in aria-label. */
|
|
481
|
+
const SETTINGS_ICON =
|
|
482
|
+
'<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>';
|
|
483
|
+
|
|
484
|
+
/** The name and the description of each need on the start page. */
|
|
485
|
+
const NEED_TEXTS: Readonly<Record<Need, readonly [keyof Messages, keyof Messages]>> = {
|
|
486
|
+
"kurze-schritte": ["needKurzeSchritte", "needKurzeSchritteAbout"],
|
|
487
|
+
"wenig-lesen": ["needWenigLesen", "needWenigLesenAbout"],
|
|
488
|
+
"mehr-tiefe": ["needMehrTiefe", "needMehrTiefeAbout"],
|
|
489
|
+
"andere-muttersprache": ["needAndereMuttersprache", "needAndereMutterspracheAbout"],
|
|
490
|
+
"klar-und-woertlich": ["needKlarUndWoertlich", "needKlarUndWoertlichAbout"],
|
|
491
|
+
"rechnen-anschaulich": ["needRechnenAnschaulich", "needRechnenAnschaulichAbout"],
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The choice of the learner's needs for the tutor, on the start page (dec-tutor-needs): a dialog that "Einstellungen"
|
|
496
|
+
* beside the choice of the assistant opens, with a box per need and one or two sentences on what it does, the first
|
|
497
|
+
* language below `andere-muttersprache`, and whether the choice holds for every explainer or for this app only. The
|
|
498
|
+
* tutor needs JavaScript, so the dialog does too: it stays in a <template> until the client module puts it into the
|
|
499
|
+
* page, opens it and keeps the choice on the device.
|
|
500
|
+
*/
|
|
501
|
+
function needsChoice(context: Context): string {
|
|
502
|
+
const { site, lang } = context;
|
|
503
|
+
const languages = [...LANGUAGES]
|
|
504
|
+
.map((tag) => ({ tag, name: languageName(tag, lang) }))
|
|
505
|
+
.sort((a, b) => a.name.localeCompare(b.name, lang));
|
|
506
|
+
const language = [
|
|
507
|
+
`<p class="explainer-tutor-language-choice" hidden><label>${plain(context.t("needsLanguage"))} <select class="explainer-tutor-language">`,
|
|
508
|
+
`<option value="">${plain(context.t("needsLanguageNone"))}</option>`,
|
|
509
|
+
...languages.map(({ tag, name }) => `<option value="${plain(tag)}">${plain(name)}</option>`),
|
|
510
|
+
`</select></label></p>`,
|
|
511
|
+
].join("");
|
|
512
|
+
const needs = NEEDS.map((need) => {
|
|
513
|
+
const [name, about] = NEED_TEXTS[need];
|
|
514
|
+
const id = `bedarf-${need}`;
|
|
515
|
+
return [
|
|
516
|
+
`<div class="explainer-tutor-need-choice"><input type="checkbox" class="explainer-tutor-need" id="${id}" value="${need}" aria-describedby="${id}-text" />`,
|
|
517
|
+
`<label for="${id}">${plain(context.t(name))}</label>`,
|
|
518
|
+
`<p class="explainer-tutor-need-text" id="${id}-text">${plain(context.t(about))}</p></div>`,
|
|
519
|
+
need === OTHER_LANGUAGE ? language : "",
|
|
520
|
+
].join("");
|
|
521
|
+
});
|
|
522
|
+
const scope = (value: string, key: keyof Messages) =>
|
|
523
|
+
`<label><input type="radio" class="explainer-tutor-needs-scope" name="bedarf-gilt" value="${value}" /> ${plain(context.t(key))}</label>`;
|
|
524
|
+
return [
|
|
525
|
+
`<template class="explainer-tutor-needs-template">`,
|
|
526
|
+
`<dialog class="explainer-tutor-needs" data-storage="${plain(site.storage ?? "explainer")}" aria-labelledby="bedarf-titel">`,
|
|
527
|
+
`<form method="dialog">`,
|
|
528
|
+
`<h2 id="bedarf-titel">${plain(context.t("needsTitle"))}</h2>`,
|
|
529
|
+
`<p>${plain(context.t("needsAbout"))}</p>`,
|
|
530
|
+
...needs,
|
|
531
|
+
`<fieldset class="explainer-tutor-needs-where"><legend>${plain(context.t("needsScopeTitle"))}</legend>${scope("archetype", "needsScopeAll")} ${scope("app", "needsScopeApp")}</fieldset>`,
|
|
532
|
+
`<p class="explainer-tutor-note">${plain(context.t("needsPrivacy"))}</p>`,
|
|
533
|
+
`<p><button class="explainer-tutor-needs-done">${plain(context.t("needsDone"))}</button></p>`,
|
|
534
|
+
`</form>`,
|
|
535
|
+
`</dialog>`,
|
|
536
|
+
`</template>`,
|
|
537
|
+
].join("\n");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* The assistants of the app as options of a select, each by its name, and copying the prompt last (COPY). An assistant
|
|
542
|
+
* that needs pasting (`paste`) carries its explanation in data-hint, which the client module shows once it is chosen.
|
|
543
|
+
*/
|
|
544
|
+
function wayOptions(context: Context): string {
|
|
545
|
+
return [
|
|
546
|
+
...PROVIDERS.map((provider) => {
|
|
547
|
+
const hint =
|
|
548
|
+
provider.paste === true ? ` data-hint="${plain(context.t("tutorPasteHint", { name: provider.name }))}"` : "";
|
|
549
|
+
return `<option value="${plain(provider.name)}"${hint}>${plain(provider.name)}</option>`;
|
|
550
|
+
}),
|
|
551
|
+
`<option value="copy">${plain(context.t("tutorOther"))}</option>`,
|
|
552
|
+
].join("");
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* "Mit einem KI-Tutor lernen", way A to the tutor, on each content page alone, beside its title. Folded while no
|
|
557
|
+
* assistant is switched on: unfolded, a select of the app's assistants and copying the text, "Einschalten" (hidden
|
|
558
|
+
* until the client module runs; without it a plain link per assistant), that the learner uses an account of their own
|
|
559
|
+
* and the assistant is no offer of lernapps.net, and the link to the page's tutor page. Once an assistant is on
|
|
560
|
+
* (chosen here or on the start page, kept on the device), a bar in its place: "Mit <name> lernen"; it is changed or
|
|
561
|
+
* switched off on the start page. Then a status line, and what the client module needs (prompt.ts): the start prompt
|
|
562
|
+
* with BASE before its address, the page's tutor page and its Markdown, and the page's id, title, exercises and
|
|
563
|
+
* sections, for the learner's position (position.ts), the result line of "Zurück" (result.ts) and the lines of
|
|
564
|
+
* "Hilfe" (stuck.ts), whose button the client module adds to each exercise with the way chosen here.
|
|
565
|
+
*/
|
|
566
|
+
function tutorSection(context: Context): string {
|
|
567
|
+
const { site, lang, route } = context;
|
|
568
|
+
const plainLinks = PROVIDERS.map(
|
|
569
|
+
(provider) =>
|
|
570
|
+
`<a class="explainer-tutor-provider" href="${plain(plainAddress(provider.url))}" rel="noopener" target="_blank">${plain(context.t("tutorOpenWith", { name: provider.name }))}</a>`,
|
|
571
|
+
).join(" ");
|
|
572
|
+
const { page } = route;
|
|
573
|
+
const data = {
|
|
574
|
+
start: startPrompt(route),
|
|
575
|
+
base: BASE,
|
|
576
|
+
query: QUERY,
|
|
577
|
+
// the root of the language tree, from this page: the base of the prompt's address is never the page's own folder
|
|
578
|
+
root: context.to(""),
|
|
579
|
+
storage: site.storage ?? "explainer",
|
|
580
|
+
...(page === undefined
|
|
581
|
+
? {}
|
|
582
|
+
: {
|
|
583
|
+
page: {
|
|
584
|
+
id: page.id,
|
|
585
|
+
title: inLanguage(page.title, lang).replace(/\s+/g, " ").trim(),
|
|
586
|
+
exercises: pageExercises(site, page, context.descriptions).map((exercise) => ({
|
|
587
|
+
id: exercise.id,
|
|
588
|
+
...(exercise.section === undefined ? {} : { section: exercise.section }),
|
|
589
|
+
numbered: exercise.count > 0,
|
|
590
|
+
})),
|
|
591
|
+
sections: sectionsOf(page),
|
|
592
|
+
},
|
|
593
|
+
}),
|
|
594
|
+
...(page === undefined ? {} : { tutor: tutorPageOf(page), markdown: `${tutorPageOf(page)}${TUTOR_MARKDOWN}` }),
|
|
595
|
+
ways: PROVIDERS.map((provider) => ({
|
|
596
|
+
name: provider.name,
|
|
597
|
+
url: provider.url,
|
|
598
|
+
...(provider.paste === true ? { paste: true } : {}),
|
|
599
|
+
})),
|
|
600
|
+
placeholder: placeholderPrompt(lang),
|
|
601
|
+
go: context.t("tutorGo"),
|
|
602
|
+
done: context.t("tutorDone"),
|
|
603
|
+
copy: context.t("tutorCopy"),
|
|
604
|
+
copied: context.t("tutorCopied"),
|
|
605
|
+
failed: context.t("tutorCopyFailed"),
|
|
606
|
+
};
|
|
607
|
+
const tutorPage =
|
|
608
|
+
page === undefined
|
|
609
|
+
? ""
|
|
610
|
+
: `<p><a class="explainer-tutor-guide" href="${context.to(tutorPageOf(page))}">${plain(context.t("tutorPageLink"))}</a></p>`;
|
|
611
|
+
return [
|
|
612
|
+
`<div class="explainer-tutor explainer-tutor-page">`,
|
|
613
|
+
`<details class="explainer-tutor-off">`,
|
|
614
|
+
`<summary>${plain(context.t("tutorTitle"))}</summary>`,
|
|
615
|
+
`<p>${plain(context.t("tutorPageIntro"))}</p>`,
|
|
616
|
+
`<p class="explainer-tutor-actions"><span class="explainer-tutor-way" hidden><select class="explainer-tutor-select" aria-label="${plain(context.t("tutorWay"))}">${wayOptions(context)}</select> <button type="button" class="explainer-tutor-on">${plain(context.t("tutorOn"))}</button></span> <span class="explainer-tutor-plain">${plainLinks}</span></p>`,
|
|
617
|
+
`<p class="explainer-tutor-hint" role="status" aria-live="polite" hidden></p>`,
|
|
618
|
+
`<p class="explainer-tutor-note">${plain(context.t("tutorAccount"))} ${plain(context.t("tutorScope"))}</p>`,
|
|
619
|
+
tutorPage,
|
|
620
|
+
`</details>`,
|
|
621
|
+
`<p class="explainer-tutor-bar" hidden><button type="button" class="explainer-tutor-go"></button></p>`,
|
|
622
|
+
`<p class="explainer-tutor-status" role="status" aria-live="polite"></p>`,
|
|
623
|
+
`<script type="application/json" class="explainer-tutor-prompt">${json(data)}</script>`,
|
|
624
|
+
`</div>`,
|
|
625
|
+
]
|
|
626
|
+
.filter((part) => part !== "")
|
|
627
|
+
.join("\n");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function head(context: Context): string {
|
|
631
|
+
const { route, site } = context;
|
|
632
|
+
const app = inLanguage(site.app.title, context.lang);
|
|
633
|
+
const title = route.kind === "start" ? app : `${route.title} – ${app}`;
|
|
634
|
+
const description =
|
|
635
|
+
route.page?.description !== undefined
|
|
636
|
+
? inLanguage(route.page.description, context.lang)
|
|
637
|
+
: route.chapter?.summary !== undefined && route.kind === "chapter"
|
|
638
|
+
? inLanguage(route.chapter.summary, context.lang)
|
|
639
|
+
: inLanguage(site.app.intro, context.lang);
|
|
640
|
+
const alternates = alternateLinks({ ...route, languages: site.app.languages }).join("\n ");
|
|
641
|
+
return [
|
|
642
|
+
`<title>${plain(title)}</title>`,
|
|
643
|
+
`<meta name="description" content="${plain(description.replace(/\s+/g, " ").trim())}" />`,
|
|
644
|
+
`<meta name="lernapps-archetype" content="explainer" />`,
|
|
645
|
+
alternates,
|
|
646
|
+
].join("\n ");
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function footer(context: Context): string {
|
|
650
|
+
return pageFooter({
|
|
651
|
+
footer: plain(context.t("footer")),
|
|
652
|
+
imprint: plain(context.t("imprint")),
|
|
653
|
+
privacy: plain(context.t("privacy")),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** One page of the app, with the markup of its components as the build rendered them. */
|
|
658
|
+
export function renderPage(
|
|
659
|
+
site: Site,
|
|
660
|
+
route: Route,
|
|
661
|
+
islands: Islands = new Map(),
|
|
662
|
+
descriptions: Descriptions = new Map(),
|
|
663
|
+
): RenderedPage {
|
|
664
|
+
const context = new Context(site, route, islands, descriptions);
|
|
665
|
+
const body = [
|
|
666
|
+
header(context),
|
|
667
|
+
breadcrumbs(context),
|
|
668
|
+
`<main class="explainer" id="inhalt-der-seite">`,
|
|
669
|
+
main(context),
|
|
670
|
+
`</main>`,
|
|
671
|
+
pager(context),
|
|
672
|
+
footer(context),
|
|
673
|
+
]
|
|
674
|
+
.filter((part) => part !== "")
|
|
675
|
+
.join("\n");
|
|
676
|
+
return { route, head: head(context), body, islands: context.hasIslands };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Every page of the app, in every language. */
|
|
680
|
+
export function renderSite(
|
|
681
|
+
site: Site,
|
|
682
|
+
islands: Islands = new Map(),
|
|
683
|
+
descriptions: Descriptions = new Map(),
|
|
684
|
+
): RenderedPage[] {
|
|
685
|
+
return routes(site).map((route) => renderPage(site, route, islands, descriptions));
|
|
686
|
+
}
|