@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,69 @@
|
|
|
1
|
+
// Formulas at build time: the LaTeX subset of the formula command list, checked, then rendered by Temml to MathML,
|
|
2
|
+
// which current browsers show and screen readers read without a script or a font. \ce{…} and \pu{…} come from
|
|
3
|
+
// Temml's mhchem extension, which registers itself on a global temml; it is loaded once, here.
|
|
4
|
+
import temml from "temml";
|
|
5
|
+
|
|
6
|
+
(globalThis as { temml?: typeof temml }).temml = temml;
|
|
7
|
+
const mhchem: string = "temml/contrib/mhchem/mhchem.js";
|
|
8
|
+
await import(mhchem);
|
|
9
|
+
|
|
10
|
+
/** The commands a formula may use (explainer.v1.schema.json, $defs/latex), besides ^, _, braces and parentheses. */
|
|
11
|
+
export const COMMANDS: readonly string[] = [
|
|
12
|
+
"\\frac",
|
|
13
|
+
"\\cdot",
|
|
14
|
+
"\\times",
|
|
15
|
+
"\\div",
|
|
16
|
+
"\\%",
|
|
17
|
+
"\\text",
|
|
18
|
+
"\\sqrt",
|
|
19
|
+
"\\approx",
|
|
20
|
+
"\\pm",
|
|
21
|
+
"\\le",
|
|
22
|
+
"\\ge",
|
|
23
|
+
"\\neq",
|
|
24
|
+
"\\,",
|
|
25
|
+
"\\{",
|
|
26
|
+
"\\}",
|
|
27
|
+
"\\ce",
|
|
28
|
+
"\\pu",
|
|
29
|
+
];
|
|
30
|
+
const allowed = new Set(COMMANDS);
|
|
31
|
+
|
|
32
|
+
/** The commands of `tex` outside the arguments of \ce and \pu, which mhchem reads in its own notation. */
|
|
33
|
+
function commands(tex: string): string[] {
|
|
34
|
+
const found: string[] = [];
|
|
35
|
+
for (let at = 0; at < tex.length; at++) {
|
|
36
|
+
if (tex[at] !== "\\") continue;
|
|
37
|
+
const word = /^\\(?:[a-zA-Z]+|.)/.exec(tex.slice(at))?.[0] ?? "\\";
|
|
38
|
+
found.push(word);
|
|
39
|
+
at += word.length - 1;
|
|
40
|
+
if ((word === "\\ce" || word === "\\pu") && tex[at + 1] === "{") {
|
|
41
|
+
let depth = 0;
|
|
42
|
+
for (at++; at < tex.length; at++) {
|
|
43
|
+
if (tex[at] === "{") depth++;
|
|
44
|
+
else if (tex[at] === "}" && --depth === 0) break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return found;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** What is wrong with the formula `tex`, or undefined when it is in the command list and parses. */
|
|
52
|
+
export function formulaProblem(tex: string): string | undefined {
|
|
53
|
+
const unknown = commands(tex).find((command) => !allowed.has(command));
|
|
54
|
+
if (unknown !== undefined) {
|
|
55
|
+
return `${unknown} is not in the formula command list; write the formula with the commands the schema lists ($defs/latex)`;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
temml.renderToString(tex, { throwOnError: true });
|
|
59
|
+
return undefined;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const message = (error instanceof Error ? error.message : String(error)).split("\n")[0]?.trim() ?? "";
|
|
62
|
+
return `the formula does not parse: ${message}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The formula as MathML: a <math> element, on its own line when `display`. */
|
|
67
|
+
export function renderFormula(tex: string, display: boolean): string {
|
|
68
|
+
return temml.renderToString(tex, { displayMode: display, throwOnError: true });
|
|
69
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Restricted Markdown, the format of every learner text with formatting: paragraphs, **bold**, *italics*, lists,
|
|
2
|
+
// links, line breaks and formulas as $…$. Everything else Markdown knows is a problem with the block to use instead,
|
|
3
|
+
// so that structure comes only from section blocks and the heading levels from the nesting. Raw HTML is a problem
|
|
4
|
+
// too. Rendered at build time; formulas become MathML.
|
|
5
|
+
import { Marked, type Token, type Tokens } from "marked";
|
|
6
|
+
import { formulaProblem, renderFormula } from "./formula.ts";
|
|
7
|
+
import { escape } from "../html.ts";
|
|
8
|
+
|
|
9
|
+
/** A problem in a text, `line` lines below its first. */
|
|
10
|
+
export interface TextProblem {
|
|
11
|
+
line: number;
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** What a text holds besides its prose: its problems, the formulas and the targets of its links. */
|
|
16
|
+
export interface Inspection {
|
|
17
|
+
problems: TextProblem[];
|
|
18
|
+
formulas: string[];
|
|
19
|
+
links: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface MathToken extends Tokens.Generic {
|
|
23
|
+
type: "math";
|
|
24
|
+
raw: string;
|
|
25
|
+
tex: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Where a link leads from the page being rendered; set for each render. */
|
|
29
|
+
let resolveLink: (href: string) => string = (href) => href;
|
|
30
|
+
|
|
31
|
+
const marked = new Marked({
|
|
32
|
+
gfm: true,
|
|
33
|
+
breaks: false,
|
|
34
|
+
extensions: [
|
|
35
|
+
{
|
|
36
|
+
name: "math",
|
|
37
|
+
level: "inline",
|
|
38
|
+
start(source: string) {
|
|
39
|
+
const at = source.indexOf("$");
|
|
40
|
+
return at < 0 ? undefined : at;
|
|
41
|
+
},
|
|
42
|
+
tokenizer(source: string): MathToken | undefined {
|
|
43
|
+
const match = /^\$((?:\\.|[^\\$\n])+?)\$/.exec(source);
|
|
44
|
+
return match === null ? undefined : { type: "math", raw: match[0], tex: match[1] ?? "" };
|
|
45
|
+
},
|
|
46
|
+
renderer(token: Tokens.Generic) {
|
|
47
|
+
return renderFormula(String(token["tex"]), false);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
renderer: {
|
|
52
|
+
html(token) {
|
|
53
|
+
return escape(token.text);
|
|
54
|
+
},
|
|
55
|
+
link({ href, tokens }) {
|
|
56
|
+
const text = this.parser.parseInline(tokens);
|
|
57
|
+
const external = /^https?:\/\//.test(href);
|
|
58
|
+
return `<a href="${escape(external ? href : resolveLink(href))}"${external ? ' rel="noopener"' : ""}>${text}</a>`;
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/** What each construct outside restricted Markdown is, and what to use instead. */
|
|
64
|
+
const NOT_ALLOWED: Readonly<Record<string, string>> = {
|
|
65
|
+
blockquote: "a quotation in Markdown; use a quote block",
|
|
66
|
+
code: "code in a text; use a code block",
|
|
67
|
+
codespan: "code in a text; use a code block, or $…$ for a formula",
|
|
68
|
+
def: "a link definition; write the link in place: [text](url)",
|
|
69
|
+
del: "strikethrough is not part of restricted Markdown",
|
|
70
|
+
hr: "a horizontal rule; structure the page with section blocks",
|
|
71
|
+
image: "an image in a text; use a figure block",
|
|
72
|
+
table: "a table in Markdown; use a table block",
|
|
73
|
+
checkbox: "a task list is not part of restricted Markdown",
|
|
74
|
+
};
|
|
75
|
+
const ALLOWED = new Set([
|
|
76
|
+
"space",
|
|
77
|
+
"paragraph",
|
|
78
|
+
"text",
|
|
79
|
+
"list",
|
|
80
|
+
"list_item",
|
|
81
|
+
"strong",
|
|
82
|
+
"em",
|
|
83
|
+
"link",
|
|
84
|
+
"escape",
|
|
85
|
+
"br",
|
|
86
|
+
"math",
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
function children(token: Token): Token[] {
|
|
90
|
+
const nested: Token[] = [];
|
|
91
|
+
if ("tokens" in token && Array.isArray(token.tokens)) nested.push(...(token.tokens as Token[]));
|
|
92
|
+
if (token.type === "list") nested.push(...token.items);
|
|
93
|
+
return nested;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The problems, formulas and links of a text in restricted Markdown. */
|
|
97
|
+
export function inspect(source: string): Inspection {
|
|
98
|
+
const found: Inspection = { problems: [], formulas: [], links: [] };
|
|
99
|
+
const lineOf = (raw: string) => {
|
|
100
|
+
const at = source.indexOf(raw.trimStart());
|
|
101
|
+
return at < 0 ? 0 : source.slice(0, at).split("\n").length - 1;
|
|
102
|
+
};
|
|
103
|
+
const add = (raw: string, message: string) => {
|
|
104
|
+
if (!found.problems.some((problem) => problem.message === message)) {
|
|
105
|
+
found.problems.push({ line: lineOf(raw), message });
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const visit = (tokens: Token[]) => {
|
|
109
|
+
for (const token of tokens) {
|
|
110
|
+
if (token.type === "heading") {
|
|
111
|
+
const title = (token as Tokens.Heading).text.trim();
|
|
112
|
+
add(token.raw, `a Markdown heading ("${title}"); use a section block with the title "${title}"`);
|
|
113
|
+
} else if (token.type === "html") {
|
|
114
|
+
const tag = /^<\/?([a-zA-Z][\w-]*)/.exec(token.raw.trim())?.[1] ?? "";
|
|
115
|
+
const first = found.problems.some((problem) => problem.message.startsWith("raw HTML"));
|
|
116
|
+
if (!first) {
|
|
117
|
+
add(
|
|
118
|
+
token.raw,
|
|
119
|
+
`raw HTML (<${tag}>) is not allowed; write restricted Markdown: **bold**, *italics*, lists, links`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
} else if (token.type === "math") {
|
|
123
|
+
const tex = String((token as MathToken).tex);
|
|
124
|
+
found.formulas.push(tex);
|
|
125
|
+
const problem = formulaProblem(tex);
|
|
126
|
+
if (problem !== undefined) add(token.raw, `$${tex}$: ${problem}`);
|
|
127
|
+
} else if (token.type === "link") {
|
|
128
|
+
const href = (token as Tokens.Link).href;
|
|
129
|
+
if (/^https?:\/\/\S+$/.test(href) || href.startsWith("/")) found.links.push(href);
|
|
130
|
+
else {
|
|
131
|
+
add(
|
|
132
|
+
token.raw,
|
|
133
|
+
`the link ${href} leads nowhere a learner can follow; link to https://… or to a page of the app: /<chapter>/<page>/`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
} else if (!ALLOWED.has(token.type)) {
|
|
137
|
+
add(token.raw, NOT_ALLOWED[token.type] ?? `${token.type} is not part of restricted Markdown`);
|
|
138
|
+
}
|
|
139
|
+
visit(children(token));
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
visit(marked.lexer(source));
|
|
143
|
+
return found;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A text in restricted Markdown as HTML; links to pages of the app go through `resolve`. */
|
|
147
|
+
export function renderMarkdown(source: string, resolve: (href: string) => string): string {
|
|
148
|
+
resolveLink = resolve;
|
|
149
|
+
return marked.parse(source, { async: false }).trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A short text in restricted Markdown as HTML without a paragraph around it, e.g. a table cell or a caption. */
|
|
153
|
+
export function renderInline(source: string, resolve: (href: string) => string): string {
|
|
154
|
+
resolveLink = resolve;
|
|
155
|
+
return marked.parseInline(source, { async: false }).trim();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A plain text as HTML. */
|
|
159
|
+
export const plain = escape;
|
|
160
|
+
|
|
161
|
+
export const FORMULA = /\$((?:\\.|[^\\$\n])+?)\$/g;
|
|
162
|
+
|
|
163
|
+
/** A plain text as HTML in which only $…$ is a formula (MathML); everything else stays text, as in a quiz. */
|
|
164
|
+
export function renderWithFormulas(text: string): string {
|
|
165
|
+
let html = "";
|
|
166
|
+
let from = 0;
|
|
167
|
+
for (const match of text.matchAll(FORMULA)) {
|
|
168
|
+
html += escape(text.slice(from, match.index)) + renderFormula(match[1] ?? "", false);
|
|
169
|
+
from = match.index + match[0].length;
|
|
170
|
+
}
|
|
171
|
+
return html + escape(text.slice(from));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** What is wrong with the formulas $…$ of a plain text; none when each is in the command list and parses. */
|
|
175
|
+
export function formulaProblems(text: string): string[] {
|
|
176
|
+
return [...text.matchAll(FORMULA)].flatMap((match) => {
|
|
177
|
+
const problem = formulaProblem(match[1] ?? "");
|
|
178
|
+
return problem === undefined ? [] : [`$${match[1]}$: ${problem}`];
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The first HTML tag in a plain text, if it has one. */
|
|
183
|
+
export function htmlTag(text: string): string | undefined {
|
|
184
|
+
return /<\/?([a-zA-Z][\w-]*)[^>]*>/.exec(text)?.[1];
|
|
185
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// The text alternative of a formula, for a name a screen reader or the accessibility check can read where the page has
|
|
2
|
+
// only MathML (an option of a choice whose text is a formula, see the label of its radio): Speech Rule Engine speaks
|
|
3
|
+
// the MathML of the formula in the page's language. Build time only; the engine loads its rules for every language
|
|
4
|
+
// of the runtime once, here, so that speaking a formula in any of them is then synchronous.
|
|
5
|
+
// @ts-expect-error speech-rule-engine ships no types; the part this file uses is typed below (drop this line if it does)
|
|
6
|
+
import untyped from "speech-rule-engine";
|
|
7
|
+
import { isLanguage, LANGUAGES } from "../languages.ts";
|
|
8
|
+
import { renderFormula } from "./formula.ts";
|
|
9
|
+
import { FORMULA } from "./markdown.ts";
|
|
10
|
+
|
|
11
|
+
/** The part of Speech Rule Engine that this file uses. */
|
|
12
|
+
interface Engine {
|
|
13
|
+
/** Loads the rules of the locale; once loaded, switching to it takes effect at once. */
|
|
14
|
+
setupEngine(setup: { locale: string; domain: string; modality: string }): Promise<void>;
|
|
15
|
+
/** The words of a MathML string, in the locale set up last. */
|
|
16
|
+
toSpeech(mathml: string): string;
|
|
17
|
+
}
|
|
18
|
+
const SRE: Engine = untyped;
|
|
19
|
+
|
|
20
|
+
const SPEECH = { domain: "clearspeak", modality: "speech" } as const;
|
|
21
|
+
|
|
22
|
+
for (const locale of LANGUAGES) await SRE.setupEngine({ locale, ...SPEECH });
|
|
23
|
+
|
|
24
|
+
/** The formula `tex` as words in `lang` (German for a language the runtime has no texts for), e.g. "2 hoch 7". */
|
|
25
|
+
export function speakFormula(tex: string, lang: string): string {
|
|
26
|
+
// the rules of every language are loaded: switching to one takes effect at once
|
|
27
|
+
void SRE.setupEngine({ locale: isLanguage(lang) ? lang : "de", ...SPEECH });
|
|
28
|
+
return SRE.toSpeech(renderFormula(tex, false)).replace(/\s+/g, " ").trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The words of a text that is only formulas, $…$ (and space); undefined for any other text, which has words of its own
|
|
33
|
+
* for a name to be made of.
|
|
34
|
+
*/
|
|
35
|
+
export function formulaAlternative(text: string, lang: string): string | undefined {
|
|
36
|
+
const formulas = [...text.matchAll(FORMULA)];
|
|
37
|
+
if (formulas.length === 0 || text.replace(FORMULA, "").trim() !== "") return undefined;
|
|
38
|
+
return formulas.map((formula) => speakFormula(formula[1] ?? "", lang)).join(" ");
|
|
39
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "urn:lernapps:schema:text.v1",
|
|
4
|
+
"title": "A learner text",
|
|
5
|
+
"description": "The texts of the content of an app: a text, and restricted Markdown, each a string while German is the only language of the app, else a map of texts by language. The schemas of the quiz, of the explainer and of the blocks they share refer to it.",
|
|
6
|
+
"$defs": {
|
|
7
|
+
"text": {
|
|
8
|
+
"description": "A text for learners without formatting: a string while German is the only language of the app, else a map of texts by language ({ de: …, en: … }) with every declared language.",
|
|
9
|
+
"anyOf": [
|
|
10
|
+
{
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"pattern": "\\S"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"type": "object",
|
|
17
|
+
"minProperties": 1,
|
|
18
|
+
"propertyNames": {
|
|
19
|
+
"pattern": "^[a-z]{2}$"
|
|
20
|
+
},
|
|
21
|
+
"additionalProperties": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"minLength": 1,
|
|
24
|
+
"pattern": "\\S"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"markdown": {
|
|
30
|
+
"description": "Restricted Markdown, as a text or a map of texts by language: paragraphs, **bold**, *italics*, lists, links and line breaks, formulas as $…$ in the formula command list. No headings (use a section block), no HTML, no images (use a figure block), no code (use a code block), no quotes (use a quote block), no tables (use a table block). A link to a page of the app starts with / and names it from the app's root: /<chapter>/<page>/#<section>.",
|
|
31
|
+
"$ref": "urn:lernapps:schema:text.v1#/$defs/text"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|