@lernapps/tooling 0.0.0-stage → 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 +67 -2
- package/archetypes/shared/a11y.ts +38 -0
- package/archetypes/shared/hooks/pre-commit +22 -0
- package/archetypes/shared/hooks/pre-push +4 -0
- package/archetypes/shared/i18n.ts +25 -0
- package/archetypes/shared/install-hooks.ts +63 -0
- package/archetypes/shared/playwright.ts +54 -0
- package/archetypes/shared/preset.ts +81 -0
- package/archetypes/shared/storage.ts +167 -0
- package/archetypes/shared/tsconfig.json +27 -0
- package/check/validation-report.v1.schema.json +116 -0
- package/dist/a11y.mjs +30 -0
- package/dist/check/rules/accessible.mjs +18 -0
- package/dist/check/rules/app-content-guide.mjs +69 -0
- package/dist/check/rules/dependency-licence.mjs +82 -0
- package/dist/check/rules/entry-fitness-matches.mjs +22 -0
- package/dist/check/rules/entry-links-resolve.mjs +44 -0
- package/dist/check/rules/explainer-readable-without-js.mjs +66 -0
- package/dist/check/rules/links-resolve.mjs +10 -0
- package/dist/check/rules/lockfile-matches.mjs +57 -0
- package/dist/check/rules/no-request-before-click.mjs +17 -0
- package/dist/check/rules/no-tracking.mjs +17 -0
- package/dist/check/rules/own-data-only.mjs +66 -0
- package/dist/check/rules/pages-reachable.mjs +25 -0
- package/dist/check/rules/usable-at-360px.mjs +19 -0
- package/dist/check-cv4I6CCH.mjs +28 -0
- package/dist/cli.mjs +3118 -0
- package/dist/i18n.mjs +11 -0
- package/dist/lint.mjs +528 -0
- package/dist/plan-DWKayEnY.mjs +75 -0
- package/dist/playwright.mjs +47 -0
- package/dist/preset.mjs +60 -0
- package/dist/skills/lernapps-app/SKILL.md +49 -0
- package/dist/skills/lernapps-cli/SKILL.md +156 -0
- package/dist/skills/lernapps-cli/references/platform-rules.md +258 -0
- package/dist/skills/lernapps-commit/SKILL.md +39 -0
- package/dist/skills/lernapps-content-growth/SKILL.md +33 -0
- package/dist/skills/lernapps-content-questions/SKILL.md +133 -0
- package/dist/skills/lernapps-content-questions/references/question-types.md +69 -0
- package/dist/skills/lernapps-content-research/SKILL.md +50 -0
- package/dist/skills/lernapps-content-research/references/sources.md +16 -0
- package/dist/skills/lernapps-exercise-patterns/SKILL.md +74 -0
- package/dist/skills/lernapps-exercise-patterns/references/biology-geography.md +13 -0
- package/dist/skills/lernapps-exercise-patterns/references/chemistry.md +16 -0
- package/dist/skills/lernapps-exercise-patterns/references/computer-science.md +13 -0
- package/dist/skills/lernapps-exercise-patterns/references/figures.md +10 -0
- package/dist/skills/lernapps-exercise-patterns/references/history.md +13 -0
- package/dist/skills/lernapps-exercise-patterns/references/mathematics.md +19 -0
- package/dist/skills/lernapps-exercise-patterns/references/page-structures.md +8 -0
- package/dist/skills/lernapps-exercise-patterns/references/physics.md +18 -0
- package/dist/skills/lernapps-exercise-patterns/references/success.md +28 -0
- package/dist/skills/lernapps-explainer/SKILL.md +33 -0
- package/dist/skills/lernapps-explainer/references/blocks.md +83 -0
- package/dist/skills/lernapps-explainer/references/components.md +66 -0
- package/dist/skills/lernapps-explainer/references/exercises.md +115 -0
- package/dist/skills/lernapps-explainer/references/rules.md +285 -0
- package/dist/skills/lernapps-explainer/references/runtime.md +79 -0
- package/dist/skills/lernapps-explainer/references/structure.md +24 -0
- package/dist/skills/lernapps-explainer-design/SKILL.md +57 -0
- package/dist/skills/lernapps-explainer-design/references/rules.md +75 -0
- package/dist/skills/lernapps-explainer-self-assessment/SKILL.md +93 -0
- package/dist/skills/lernapps-github/SKILL.md +102 -0
- package/dist/skills/lernapps-interactive/SKILL.md +141 -0
- package/dist/skills/lernapps-interactive/references/frame.md +30 -0
- package/dist/skills/lernapps-interactive-design/SKILL.md +58 -0
- package/dist/skills/lernapps-listing/SKILL.md +44 -0
- package/dist/skills/lernapps-listing/references/keeping-updated.md +33 -0
- package/dist/skills/lernapps-quiz/SKILL.md +100 -0
- package/dist/skills/lernapps-quiz-design/SKILL.md +48 -0
- package/dist/status-CXoQBgfU.mjs +76 -0
- package/dist/storage.mjs +113 -0
- package/generator/app/.agents/skills/app-content/SKILL.md +63 -0
- package/generator/app/.github/ISSUE_TEMPLATE/inhaltsfehler.yml +25 -0
- package/generator/app/.github/workflows/pages.yml +61 -0
- package/generator/app/LICENSE +21 -0
- package/generator/app/renovate.json +4 -0
- package/generator/architecture.ts +212 -0
- package/generator/create.ts +462 -0
- package/generator/init.ts +186 -0
- package/generator/update.ts +429 -0
- package/guidance/AGENTS.md +26 -0
- package/guidance/increment-template.md +194 -0
- package/guidance/plan-front-matter.v1.schema.json +36 -0
- package/guidance/plan-template.md +233 -0
- package/guidance/subagent-brief.md +25 -0
- package/guidance/workflow-rules.md +109 -0
- package/package.json +135 -4
- package/review/verdict.v1.schema.json +134 -0
- package/skills/archetypes/lernapps-explainer/SKILL.md +33 -0
- package/skills/archetypes/lernapps-explainer/references/blocks.md +83 -0
- package/skills/archetypes/lernapps-explainer/references/components.md +66 -0
- package/skills/archetypes/lernapps-explainer/references/exercises.md +115 -0
- package/skills/archetypes/lernapps-explainer/references/rules.md +285 -0
- package/skills/archetypes/lernapps-explainer/references/runtime.md +79 -0
- package/skills/archetypes/lernapps-explainer/references/structure.md +24 -0
- package/skills/archetypes/lernapps-explainer-design/SKILL.md +57 -0
- package/skills/archetypes/lernapps-explainer-design/references/rules.md +75 -0
- package/skills/archetypes/lernapps-explainer-self-assessment/SKILL.md +93 -0
- package/skills/archetypes/lernapps-interactive/SKILL.md +141 -0
- package/skills/archetypes/lernapps-interactive/references/frame.md +30 -0
- package/skills/archetypes/lernapps-interactive-design/SKILL.md +58 -0
- package/skills/archetypes/lernapps-quiz/SKILL.md +100 -0
- package/skills/archetypes/lernapps-quiz-design/SKILL.md +48 -0
- package/skills/content/lernapps-content-questions/SKILL.md +133 -0
- package/skills/content/lernapps-content-questions/references/question-types.md +69 -0
- package/skills/content/lernapps-content-research/SKILL.md +50 -0
- package/skills/content/lernapps-content-research/references/sources.md +16 -0
- package/skills/content/lernapps-exercise-patterns/SKILL.md +74 -0
- package/skills/content/lernapps-exercise-patterns/references/biology-geography.md +13 -0
- package/skills/content/lernapps-exercise-patterns/references/chemistry.md +16 -0
- package/skills/content/lernapps-exercise-patterns/references/computer-science.md +13 -0
- package/skills/content/lernapps-exercise-patterns/references/figures.md +10 -0
- package/skills/content/lernapps-exercise-patterns/references/history.md +13 -0
- package/skills/content/lernapps-exercise-patterns/references/mathematics.md +19 -0
- package/skills/content/lernapps-exercise-patterns/references/page-structures.md +8 -0
- package/skills/content/lernapps-exercise-patterns/references/physics.md +18 -0
- package/skills/content/lernapps-exercise-patterns/references/success.md +28 -0
- package/skills/process/lernapps-app/SKILL.md +49 -0
- package/skills/process/lernapps-commit/SKILL.md +39 -0
- package/skills/process/lernapps-content-growth/SKILL.md +33 -0
- package/skills/process/lernapps-github/SKILL.md +102 -0
- package/skills/process/lernapps-listing/SKILL.md +44 -0
- package/skills/process/lernapps-listing/references/keeping-updated.md +33 -0
- package/skills/tooling/lernapps-cli/SKILL.md +156 -0
- package/skills/tooling/lernapps-cli/references/platform-rules.md +258 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lernapps-quiz
|
|
3
|
+
description: Use in Code of a lernapps.net quiz whenever you write or change the question bank src/quiz.json, add a language, or a build or runtime test of the quiz fails. What you write, how the runtime shows and tests the bank, languages, and the quiz's technical rules. The question types and how to write good questions are in lernapps-content-questions.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Quiz
|
|
8
|
+
|
|
9
|
+
- You write `src/quiz.json` and, for the content of its questions, the image files in `src/images/`. Engine, question
|
|
10
|
+
types, feedback, scoring, order, accessibility and the no-JS page come from the runtime `@lernapps/app-templates`.
|
|
11
|
+
- Schema: `node_modules/@lernapps/app-templates/runtime/quiz/quiz.v1.schema.json`.
|
|
12
|
+
- `create` writes the example bank "Naturwunder der Welt": replace it with the planned questions.
|
|
13
|
+
- The fields of each question type and how to write good questions: skill `lernapps-content-questions`.
|
|
14
|
+
|
|
15
|
+
## Gotchas
|
|
16
|
+
|
|
17
|
+
- The build validates the bank; the runtime's tests then answer every question in the browser. A runtime test that
|
|
18
|
+
fails while the bank validates is a template bug (skill `lernapps-cli`).
|
|
19
|
+
|
|
20
|
+
## Runtime
|
|
21
|
+
|
|
22
|
+
### Build and page
|
|
23
|
+
|
|
24
|
+
- Build reads, validates, renders every question into `index.html`, with the content it shows (`material`), and
|
|
25
|
+
carries the images of `src/images/` to `images/`.
|
|
26
|
+
- No JS: a worksheet; all questions, then solutions with background and links.
|
|
27
|
+
- JS: one question at a time, right or wrong shown; then each option's `info` (right ones and the choice marked),
|
|
28
|
+
the explanation, the links. Score and solutions at the end.
|
|
29
|
+
- Order shuffled by `?seed=42` (same link, same order). Deep link to a question: `#frage-<id>`.
|
|
30
|
+
|
|
31
|
+
### Storage
|
|
32
|
+
|
|
33
|
+
- Stores nothing, unless `"rememberLastResult": true`: then the last score stays on the device.
|
|
34
|
+
|
|
35
|
+
### Tests
|
|
36
|
+
|
|
37
|
+
- From the runtime (`e2e/quiz.e2e.ts`), run by `lernapps check`. Per question: deep link, a right and a wrong answer,
|
|
38
|
+
every `info`, explanation and links shown. Also full score and the no-JS page.
|
|
39
|
+
- Each further language: its no-JS page (questions, solutions, switch) and one answered question in that language.
|
|
40
|
+
- New runtime features or fixes: `lernapps update` (skill `lernapps-cli`).
|
|
41
|
+
|
|
42
|
+
### A runtime test fails
|
|
43
|
+
|
|
44
|
+
- Bank validates (build passes) but a runtime test fails = template bug: act as skill `lernapps-cli` says.
|
|
45
|
+
- Never change the runtime in `node_modules` (`quiz-only-data`). Go on with the questions.
|
|
46
|
+
- A question shows an image or a table with `material`: skill `lernapps-content-questions`, its reference question-types.
|
|
47
|
+
|
|
48
|
+
### Languages
|
|
49
|
+
|
|
50
|
+
- German; more with `"languages": ["de", "en"]`, German first (runtime texts: de, en).
|
|
51
|
+
- Then every learner text is a map with each language: title, intro, source titles; per question text, options,
|
|
52
|
+
`info`, items, pairs, unit, explanation, link titles.
|
|
53
|
+
- Without `languages`: German strings.
|
|
54
|
+
- Link `url`: one address, or a map per language (own Wikipedia).
|
|
55
|
+
- Build errors, one line each, e.g.
|
|
56
|
+
`questions[1].explanation (en) (question "totes-meer"): the translation is missing; write the text as a map: { de: …, en: … }`;
|
|
57
|
+
undeclared language: `… is not a language of the app; declare it in languages in src/quiz.json, or remove it`;
|
|
58
|
+
`languages[0]: German comes first`.
|
|
59
|
+
- Pages: German `index.html`, others `<language>/index.html`, with `lang`, `hreflang`, a no-JS language switch.
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"id": "totes-meer",
|
|
64
|
+
"type": "true-false",
|
|
65
|
+
"text": { "de": "Im Toten Meer kann man nicht untergehen.", "en": "You cannot sink in the Dead Sea." },
|
|
66
|
+
"answer": true,
|
|
67
|
+
"explanation": {
|
|
68
|
+
"de": "Das Wasser ist so salzig, dass ein Mensch an der Oberfläche treibt.",
|
|
69
|
+
"en": "The water is so salty that a person floats on the surface."
|
|
70
|
+
},
|
|
71
|
+
"links": [
|
|
72
|
+
{ "title": { "de": "Totes Meer (Wikipedia)", "en": "Dead Sea (Wikipedia)" }, "url": "https://de.wikipedia.org/wiki/Totes_Meer" }
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Rules
|
|
78
|
+
|
|
79
|
+
### `quiz-only-data`: Write only the question bank and its images
|
|
80
|
+
|
|
81
|
+
```rule
|
|
82
|
+
id: quiz-only-data
|
|
83
|
+
scope: archetype:quiz
|
|
84
|
+
severity: error
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- Change only `src/quiz.json` and add image files to `src/images/`. `index.html`, `src/main.ts`, `e2e/quiz.e2e.ts`,
|
|
88
|
+
config as generated (`keep-the-preset`).
|
|
89
|
+
- Bank cannot say it → "Where I had to guess", never code around the runtime.
|
|
90
|
+
|
|
91
|
+
### `quiz-stable-ids`: Keep the ids
|
|
92
|
+
|
|
93
|
+
```rule
|
|
94
|
+
id: quiz-stable-ids
|
|
95
|
+
scope: archetype:quiz
|
|
96
|
+
severity: warning
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- Short, telling `id` ("grand-canyon"); keep it when the text changes (shared links `#frage-<id>`).
|
|
100
|
+
- New question, new id; never reuse a removed one.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lernapps-quiz-design
|
|
3
|
+
description: Use in Explore right after choosing the `quiz` archetype - how the content of a lernapps.net quiz maps onto rounds, question types and patterns, which the plan then applies.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Designing a quiz
|
|
8
|
+
|
|
9
|
+
## What a quiz is
|
|
10
|
+
|
|
11
|
+
- One page of questions with options and feedback. The runtime shows one question at a time, then each option's
|
|
12
|
+
background, the explanation and links; the score at the end. Without JavaScript: a worksheet with solutions.
|
|
13
|
+
- You will write the question bank `src/quiz.json` and, for what a question shows, image files in `src/images/`;
|
|
14
|
+
no code (`quiz-only-data`).
|
|
15
|
+
|
|
16
|
+
## Can and cannot
|
|
17
|
+
|
|
18
|
+
- The README of the template says it: <https://raw.githubusercontent.com/lernapps/app-templates/main/quiz/README.md>.
|
|
19
|
+
Read it before this skill's mapping, if you have not yet.
|
|
20
|
+
|
|
21
|
+
## Map the content
|
|
22
|
+
|
|
23
|
+
- A chapter → a round: the questions of one chapter, next to each other in the bank.
|
|
24
|
+
- A competency → one or more questions.
|
|
25
|
+
- The bank is the shared material: each fact in one question.
|
|
26
|
+
- A pattern → one question type with a fixed form of text, options and background, varied over a round's facts.
|
|
27
|
+
- Not expressible as a question → no code: note it under "Where I had to guess"; an issue at
|
|
28
|
+
<https://github.com/lernapps/app-templates/issues> if other quizzes need it.
|
|
29
|
+
|
|
30
|
+
## Question types
|
|
31
|
+
|
|
32
|
+
| `type` | The learner |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `single-choice` | chooses one option |
|
|
35
|
+
| `multiple-choice` | chooses every correct option |
|
|
36
|
+
| `true-false` | judges a statement |
|
|
37
|
+
| `number` | types a number, within a tolerance |
|
|
38
|
+
| `ordering` | puts items in their fixed order |
|
|
39
|
+
| `matching` | matches left to right |
|
|
40
|
+
|
|
41
|
+
- Which pattern fits which competency: skill `lernapps-exercise-patterns`.
|
|
42
|
+
|
|
43
|
+
## Decide
|
|
44
|
+
|
|
45
|
+
- Languages: German; English as a second language is supported. Each declared language complete.
|
|
46
|
+
- Keep the last score on the device (`rememberLastResult`) or nothing. Keeping it → the entry says
|
|
47
|
+
`fitness.storage: device`.
|
|
48
|
+
- Order: shuffled per seed; a link with `?seed=42` gives every learner the same order.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// check/status.ts
|
|
2
|
+
/** The status a rate-limited server answers with. */
|
|
3
|
+
const RATE_LIMITED = 429;
|
|
4
|
+
const RETRIES = 2;
|
|
5
|
+
/** The longest wait before one retry, whatever the server's Retry-After says. */
|
|
6
|
+
const MAX_WAIT = 2e3;
|
|
7
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
/** The wait before retry `attempt` (0, 1, ...): the server's Retry-After in seconds, else 0.5 s doubled, at most 2 s. */
|
|
9
|
+
function wait(retryAfter, attempt) {
|
|
10
|
+
const seconds = Number(retryAfter);
|
|
11
|
+
const asked = retryAfter !== null && Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : 500 * 2 ** attempt;
|
|
12
|
+
return Math.min(asked, MAX_WAIT);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The status of `url` after redirects, and whether it is an HTML page; status 0 when it cannot be fetched. With `ids`,
|
|
16
|
+
* also the element ids of an HTML page that answers without an error, as the server sends it (no script runs).
|
|
17
|
+
*/
|
|
18
|
+
async function linkStatus(url, options = {}) {
|
|
19
|
+
for (let attempt = 0;; attempt++) {
|
|
20
|
+
let response;
|
|
21
|
+
let ids;
|
|
22
|
+
try {
|
|
23
|
+
response = await fetch(url, { redirect: "follow" });
|
|
24
|
+
const html = (response.headers.get("content-type") ?? "").includes("html");
|
|
25
|
+
if (options.ids && html && response.ok) ids = elementIds(await response.text());
|
|
26
|
+
else await response.body?.cancel();
|
|
27
|
+
} catch {
|
|
28
|
+
return {
|
|
29
|
+
status: 0,
|
|
30
|
+
html: false
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (response.status !== RATE_LIMITED || attempt >= RETRIES) return {
|
|
34
|
+
status: response.status,
|
|
35
|
+
html: (response.headers.get("content-type") ?? "").includes("html"),
|
|
36
|
+
...ids ? { ids } : {}
|
|
37
|
+
};
|
|
38
|
+
await sleep(wait(response.headers.get("retry-after"), attempt));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** The character references an attribute value may carry, decoded. */
|
|
42
|
+
const decodeReferences = (value) => value.replace(/&#x([0-9a-f]+);/gi, (_match, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/&#(\d+);/g, (_match, decimal) => String.fromCodePoint(Number(decimal))).replace(/"/g, "\"").replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
43
|
+
const VALUE = String.raw`\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))`;
|
|
44
|
+
const ID = new RegExp(String.raw`\sid${VALUE}`, "i");
|
|
45
|
+
const NAME = new RegExp(String.raw`\sname${VALUE}`, "i");
|
|
46
|
+
/** The ids of the elements of an HTML page (`id`, and `name` of an `<a>`, which a fragment also finds). */
|
|
47
|
+
function elementIds(html) {
|
|
48
|
+
const ids = /* @__PURE__ */ new Set();
|
|
49
|
+
for (const tag of html.match(/<[a-zA-Z][^>]*>/g) ?? []) for (const match of [ID.exec(tag), /^<a\s/i.test(tag) ? NAME.exec(tag) : null]) if (match) ids.add(decodeReferences(match[1] ?? match[2] ?? match[3] ?? ""));
|
|
50
|
+
return ids;
|
|
51
|
+
}
|
|
52
|
+
/** The fix of a link that could not be checked. */
|
|
53
|
+
const CHECK_AGAIN_LATER = "Nothing to fix for this: the server limits how often it may be asked. Check again later (lernapps check); the other links were checked";
|
|
54
|
+
/**
|
|
55
|
+
* The problem a check reports for `link` at `where` that answered `status`, undefined when it resolves. A broken link
|
|
56
|
+
* gets the check's `fix`; a rate-limited one is not checked: severity hint, with nothing to fix.
|
|
57
|
+
*/
|
|
58
|
+
function linkProblem(where, link, status, fix) {
|
|
59
|
+
if (status === RATE_LIMITED) return {
|
|
60
|
+
where,
|
|
61
|
+
found: `${link} could not be checked: the server answers 429 Too Many Requests (rate limited), also after ${RETRIES} retries`,
|
|
62
|
+
fix: CHECK_AGAIN_LATER,
|
|
63
|
+
severity: "hint"
|
|
64
|
+
};
|
|
65
|
+
if (status === 0) return {
|
|
66
|
+
where,
|
|
67
|
+
found: `${link} cannot be fetched`,
|
|
68
|
+
fix
|
|
69
|
+
};
|
|
70
|
+
if (status >= 400) return {
|
|
71
|
+
where,
|
|
72
|
+
found: `${link} answers ${status}`,
|
|
73
|
+
fix
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export { linkStatus as n, linkProblem as t };
|
package/dist/storage.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// archetypes/shared/storage.ts
|
|
2
|
+
/**
|
|
3
|
+
* What an app may keep under its prefix: 100 kB (100,000 bytes), measured as browsers count localStorage, 2 bytes per
|
|
4
|
+
* UTF-16 code unit of every key (with the prefix) and value.
|
|
5
|
+
*/
|
|
6
|
+
const STORAGE_QUOTA = 1e5;
|
|
7
|
+
/**
|
|
8
|
+
* The storage of the app named `app`: its id in the catalog (entries/<id>.yaml, rule own-data-only), e.g. "brueche";
|
|
9
|
+
* in localStorage, or with `type: "sessionStorage"` in sessionStorage, which the browser empties when the tab closes.
|
|
10
|
+
*/
|
|
11
|
+
function createStorage(app, options = {}) {
|
|
12
|
+
return prefixed(`lernapps:${app}:`, `lernapps:${app}:`, STORAGE_QUOTA, options.type);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The values under `prefix` in the browser's storage `type`, which may take at most `quota` bytes together with every
|
|
16
|
+
* other value under `counted` there.
|
|
17
|
+
*/
|
|
18
|
+
function prefixed(prefix, counted, quota, type = "localStorage") {
|
|
19
|
+
/** The bytes the values under `counted` take, without the one under `except`. */
|
|
20
|
+
const used = (target, except) => {
|
|
21
|
+
let bytes = 0;
|
|
22
|
+
for (let i = 0; i < target.length; i++) {
|
|
23
|
+
const key = target.key(i);
|
|
24
|
+
if (key === null || key === except || !key.startsWith(counted)) continue;
|
|
25
|
+
bytes += (key.length + (target.getItem(key)?.length ?? 0)) * 2;
|
|
26
|
+
}
|
|
27
|
+
return bytes;
|
|
28
|
+
};
|
|
29
|
+
const store = () => {
|
|
30
|
+
try {
|
|
31
|
+
return globalThis[type];
|
|
32
|
+
} catch {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
load(key, guard) {
|
|
38
|
+
try {
|
|
39
|
+
const text = store()?.getItem(prefix + key);
|
|
40
|
+
if (text === null || text === void 0) return void 0;
|
|
41
|
+
const value = JSON.parse(text);
|
|
42
|
+
return guard(value) ? value : void 0;
|
|
43
|
+
} catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
save(key, value) {
|
|
48
|
+
try {
|
|
49
|
+
const target = store();
|
|
50
|
+
if (!target) return false;
|
|
51
|
+
const text = String(JSON.stringify(value));
|
|
52
|
+
if (used(target, prefix + key) + (prefix.length + key.length + text.length) * 2 > quota) return false;
|
|
53
|
+
target.setItem(prefix + key, text);
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
remove(key) {
|
|
60
|
+
try {
|
|
61
|
+
store()?.removeItem(prefix + key);
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* What the learner's settings for every app and for every app of an archetype take together under SETTINGS_PREFIX:
|
|
68
|
+
* 10 kB (10,000 bytes), counted as STORAGE_QUOTA is. A setting for one app only counts towards that app's quota.
|
|
69
|
+
*/
|
|
70
|
+
const SETTINGS_QUOTA = 1e4;
|
|
71
|
+
/**
|
|
72
|
+
* Where the learner's settings for more than one app are kept: `lernapps:settings:<key>` for every app,
|
|
73
|
+
* `lernapps:settings:<archetype>:<key>` for every app of an archetype. `settings` is never the id of an app.
|
|
74
|
+
*/
|
|
75
|
+
const SETTINGS_PREFIX = "lernapps:settings:";
|
|
76
|
+
/**
|
|
77
|
+
* The learner's settings for the app named `app` (its id, as for `createStorage`) of the archetype `archetype`
|
|
78
|
+
* (`explainer`, as its pages mark it with `<meta name="lernapps-archetype">`): the app's own one under
|
|
79
|
+
* `lernapps:<app>:settings:<key>` goes before the archetype's, which goes before the one for every app
|
|
80
|
+
* (rule own-data-only lets an app touch these and no other archetype's). A key has no colon, so that a key for every
|
|
81
|
+
* app is never taken for an archetype's.
|
|
82
|
+
*/
|
|
83
|
+
function createSettings(app, archetype) {
|
|
84
|
+
const stores = [
|
|
85
|
+
["app", prefixed(`lernapps:${app}:settings:`, `lernapps:${app}:`, STORAGE_QUOTA)],
|
|
86
|
+
["archetype", archetype === void 0 ? void 0 : prefixed(`${SETTINGS_PREFIX}${archetype}:`, SETTINGS_PREFIX, SETTINGS_QUOTA)],
|
|
87
|
+
["all", prefixed(SETTINGS_PREFIX, SETTINGS_PREFIX, SETTINGS_QUOTA)]
|
|
88
|
+
];
|
|
89
|
+
const valid = (key) => key !== "" && !key.includes(":");
|
|
90
|
+
return {
|
|
91
|
+
load(key, guard) {
|
|
92
|
+
if (!valid(key)) return void 0;
|
|
93
|
+
for (const [scope, store] of stores) {
|
|
94
|
+
const value = store?.load(key, guard);
|
|
95
|
+
if (value !== void 0) return {
|
|
96
|
+
value,
|
|
97
|
+
scope
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
save(key, value, scope) {
|
|
102
|
+
const at = stores.findIndex(([where]) => where === scope);
|
|
103
|
+
const store = stores[at]?.[1];
|
|
104
|
+
if (!valid(key) || store === void 0 || !store.save(key, value)) return false;
|
|
105
|
+
for (const [, narrower] of stores.slice(0, at)) narrower?.remove(key);
|
|
106
|
+
return true;
|
|
107
|
+
},
|
|
108
|
+
remove(key, scope) {
|
|
109
|
+
if (valid(key)) stores.find(([where]) => where === scope)?.[1]?.remove(key);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export { SETTINGS_PREFIX, SETTINGS_QUOTA, STORAGE_QUOTA, createSettings, createStorage };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-content
|
|
3
|
+
description: Use for any content work in this app - researching, writing, extending, correcting or reviewing its competencies, chapters, pages, rounds, questions, exercises, figures, terms or sources. This app's own content decisions; they take precedence over the generic lernapps content skills. Load it before you change any content.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Content guide of this app
|
|
7
|
+
|
|
8
|
+
<!--
|
|
9
|
+
Skeleton. Delete this comment when you fill the guide.
|
|
10
|
+
- When: after the plan is confirmed, before any content. Replace every placeholder comment below; none may remain
|
|
11
|
+
(`lernapps check`, rule app-content-guide, fails once the plan is in Commit).
|
|
12
|
+
- Keep the headings.
|
|
13
|
+
- Concrete, for an agent without your conversation: files, ids, data files, one example.
|
|
14
|
+
- No: "Use varied exercises with figures where they help."
|
|
15
|
+
- Yes: "Every page ends with section `uebung`: one `generated` exercise of pattern 'Ohmsches Gesetz' from
|
|
16
|
+
`src/data/widerstaende.yaml`, one `matching` of symbols and parts."
|
|
17
|
+
- Content decision changes → update this file in the same commit.
|
|
18
|
+
- Public: no personal data, no goals of one learner.
|
|
19
|
+
-->
|
|
20
|
+
|
|
21
|
+
## Curriculum
|
|
22
|
+
|
|
23
|
+
<!-- FILL IN: Land, Schulform, Jahrgang; orientation (Lehrplan: document, issuer, year, address, page and unit per
|
|
24
|
+
part; welcome, not required); where the creator's material differs, and which one the app follows. -->
|
|
25
|
+
|
|
26
|
+
## Competencies
|
|
27
|
+
|
|
28
|
+
<!-- FILL IN: every competency as "Ich kann …", grouped by chapter; per competency: page or round (file, id),
|
|
29
|
+
exercise ids. -->
|
|
30
|
+
|
|
31
|
+
## Content structure
|
|
32
|
+
|
|
33
|
+
<!-- FILL IN: how a chapter, page or round is built: sections or question groups in order, ids or titles, what
|
|
34
|
+
each holds; length. -->
|
|
35
|
+
|
|
36
|
+
## Terminology
|
|
37
|
+
|
|
38
|
+
<!-- FILL IN: terms used and avoided (why); units, notation; how learners are addressed; glossary terms with
|
|
39
|
+
wording. -->
|
|
40
|
+
|
|
41
|
+
## Exercise patterns
|
|
42
|
+
|
|
43
|
+
<!-- FILL IN: per pattern: name, competency, archetype form (block or question type, fixed parts, what varies),
|
|
44
|
+
one example (file, id), data file. -->
|
|
45
|
+
|
|
46
|
+
## Figures
|
|
47
|
+
|
|
48
|
+
<!-- FILL IN: figure kinds and images, for what; conventions (colours, labels, units, orientation, sizes); alt
|
|
49
|
+
texts and captions; image origin and licence. -->
|
|
50
|
+
|
|
51
|
+
## Sources
|
|
52
|
+
|
|
53
|
+
<!-- FILL IN: sources linked or relied on (Lehrplan, second explanations, videos, articles), how verified, what
|
|
54
|
+
is left out; the creator's material stays local, never committed. -->
|
|
55
|
+
|
|
56
|
+
## Adding a chapter or round
|
|
57
|
+
|
|
58
|
+
<!-- FILL IN: the steps for a new chapter (explainer) or round (quiz), in order, with the files: competencies,
|
|
59
|
+
content, exercises from the patterns, entry topics, check, commit. -->
|
|
60
|
+
|
|
61
|
+
## Rules learned from mistakes
|
|
62
|
+
|
|
63
|
+
<!-- FILL IN: one line per rule with the mistake it came from. "None yet." until the first. -->
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Fehler im Inhalt melden
|
|
2
|
+
description: Eine Frage, eine Antwort oder eine Erklärung stimmt nicht.
|
|
3
|
+
title: "Fehler im Inhalt: "
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: Danke, dass du hilfst, die App besser zu machen. Schreib bitte keine Namen oder andere persönliche Daten hinein.
|
|
8
|
+
- type: input
|
|
9
|
+
id: wo
|
|
10
|
+
attributes:
|
|
11
|
+
label: Wo ist der Fehler?
|
|
12
|
+
description: Die Frage oder die Seite, am besten mit ihrem Link.
|
|
13
|
+
validations:
|
|
14
|
+
required: true
|
|
15
|
+
- type: textarea
|
|
16
|
+
id: fehler
|
|
17
|
+
attributes:
|
|
18
|
+
label: Was stimmt nicht?
|
|
19
|
+
validations:
|
|
20
|
+
required: true
|
|
21
|
+
- type: textarea
|
|
22
|
+
id: richtig
|
|
23
|
+
attributes:
|
|
24
|
+
label: Was wäre richtig?
|
|
25
|
+
description: Wenn du es weißt, mit einer Quelle, zum Beispiel einem Schulbuch.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Pages
|
|
2
|
+
|
|
3
|
+
# Checks the app and publishes it. The job `app` runs the app check of lernapps/tooling: `lernapps check`, the same
|
|
4
|
+
# command the git hooks ran (format, lint, type check, unit tests, build, the checks of the built app, the end-to-end
|
|
5
|
+
# tests), and keeps its validation report as the artifact "validation-report". On main, the job `deploy` publishes
|
|
6
|
+
# the bundle it checked with the app publish action: to the gh-pages branch, which GitHub Pages serves from its root
|
|
7
|
+
# (Settings → Pages → Deploy from a branch → gh-pages, / (root)). It checks that setting, waits until the app's
|
|
8
|
+
# address serves the new bundle, and prints the address; on a failure, its message says what to do. On a pull request
|
|
9
|
+
# from this repo, the job `preview` publishes the bundle the job `app` checked as a preview, with the app preview
|
|
10
|
+
# action: at <the app's address>pr-preview/pr-<number>/, not indexed by search engines, updated on every push and
|
|
11
|
+
# removed when the pull request is closed or merged; one comment on the pull request, in German for the creator,
|
|
12
|
+
# links it. A pull request from a fork gets no preview: its token may not write. The actions first ask what the change
|
|
13
|
+
# can affect (lernapps changes): a change of only the plan (.vibe/) or only docs (docs/**/*.md) checks, publishes and
|
|
14
|
+
# previews nothing, and the jobs succeed with a notice. So the workflow runs on every push and pull request, with no
|
|
15
|
+
# paths filter: a check skipped by a paths filter stays "Pending" and blocks a pull request that requires it. GitHub
|
|
16
|
+
# may start no run for a push that creates main from commits it already has (the first publish): then start it by
|
|
17
|
+
# hand, Actions → Pages → Run workflow on main (gh workflow run Pages --ref main), as the plan says; a run started by
|
|
18
|
+
# hand checks and publishes everything.
|
|
19
|
+
on:
|
|
20
|
+
push:
|
|
21
|
+
branches: [main]
|
|
22
|
+
pull_request:
|
|
23
|
+
types: [opened, synchronize, reopened, closed] # closed: removes the preview
|
|
24
|
+
workflow_dispatch:
|
|
25
|
+
|
|
26
|
+
permissions:
|
|
27
|
+
contents: read
|
|
28
|
+
|
|
29
|
+
concurrency:
|
|
30
|
+
group: pages-${{ github.ref }}
|
|
31
|
+
cancel-in-progress: false
|
|
32
|
+
|
|
33
|
+
jobs:
|
|
34
|
+
app:
|
|
35
|
+
if: github.event.action != 'closed'
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
steps:
|
|
38
|
+
- uses: lernapps/tooling/actions/app-check@{{tooling}}
|
|
39
|
+
deploy:
|
|
40
|
+
if: github.ref == 'refs/heads/main'
|
|
41
|
+
needs: app
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
permissions:
|
|
44
|
+
contents: write
|
|
45
|
+
pages: write
|
|
46
|
+
steps:
|
|
47
|
+
- uses: lernapps/tooling/actions/app-publish@{{tooling}}
|
|
48
|
+
preview:
|
|
49
|
+
# a pull request from this repo only; on closing, it removes the preview without a check
|
|
50
|
+
if: >-
|
|
51
|
+
always() && github.event_name == 'pull_request' &&
|
|
52
|
+
github.event.pull_request.head.repo.full_name == github.repository &&
|
|
53
|
+
(github.event.action == 'closed' || needs.app.result == 'success')
|
|
54
|
+
needs: app
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
permissions:
|
|
57
|
+
contents: write
|
|
58
|
+
pull-requests: write
|
|
59
|
+
pages: read # the app's address, for the comment
|
|
60
|
+
steps:
|
|
61
|
+
- uses: lernapps/tooling/actions/app-preview@{{tooling}}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) {{year}} {{holder}}
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|