mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
The build script now extracts content.json into derived data and copies it into the app bundle, so no generated file is referenced from the project or written into the repository. Signing is explicit: ad hoc for the simulator, automatic with your own team for a device. Co-authored-by: Cursor <cursoragent@cursor.com>
1299 lines
47 KiB
Python
1299 lines
47 KiB
Python
#!/usr/bin/env python3
|
||
"""Pull educational content out of the GTK C sources into content.json."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
SRC = ROOT / "src"
|
||
# Default target is the Android asset; Xcode passes `--out` and copies the
|
||
# file into the app bundle itself, so nothing generated lives under ios/.
|
||
OUTS = [
|
||
ROOT / "android" / "app" / "src" / "main" / "assets" / "content.json",
|
||
]
|
||
if "--out" in sys.argv:
|
||
OUTS = [Path(a) for a in sys.argv[sys.argv.index("--out") + 1:] if a]
|
||
if not OUTS:
|
||
sys.exit("usage: extract-android-content.py [--out PATH ...]")
|
||
OUT = OUTS[0]
|
||
|
||
IDENT = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
||
|
||
|
||
def strip_comments(src: str) -> str:
|
||
out = []
|
||
i, n = 0, len(src)
|
||
while i < n:
|
||
c = src[i]
|
||
if c == '"' or c == "'":
|
||
q = c
|
||
out.append(c)
|
||
i += 1
|
||
while i < n:
|
||
ch = src[i]
|
||
out.append(ch)
|
||
if ch == "\\" and i + 1 < n:
|
||
out.append(src[i + 1])
|
||
i += 2
|
||
continue
|
||
i += 1
|
||
if ch == q:
|
||
break
|
||
continue
|
||
if c == "/" and i + 1 < n and src[i + 1] == "/":
|
||
i += 2
|
||
while i < n and src[i] != "\n":
|
||
i += 1
|
||
continue
|
||
if c == "/" and i + 1 < n and src[i + 1] == "*":
|
||
i += 2
|
||
while i + 1 < n and not (src[i] == "*" and src[i + 1] == "/"):
|
||
i += 1
|
||
i = min(n, i + 2)
|
||
out.append(" ")
|
||
continue
|
||
out.append(c)
|
||
i += 1
|
||
return "".join(out)
|
||
|
||
|
||
def unescape(s: str) -> str:
|
||
out = []
|
||
i = 0
|
||
while i < len(s):
|
||
if s[i] == "\\" and i + 1 < len(s):
|
||
n = s[i + 1]
|
||
mapping = {
|
||
"n": "\n",
|
||
"t": "\t",
|
||
"r": "\r",
|
||
"\\": "\\",
|
||
'"': '"',
|
||
"'": "'",
|
||
}
|
||
if n in mapping:
|
||
out.append(mapping[n])
|
||
i += 2
|
||
continue
|
||
if n == "x":
|
||
j = i + 2
|
||
hexdigits = ""
|
||
while j < len(s) and s[j] in "0123456789abcdefABCDEF" and len(hexdigits) < 2:
|
||
hexdigits += s[j]
|
||
j += 1
|
||
out.append(chr(int(hexdigits, 16)) if hexdigits else "x")
|
||
i = j
|
||
continue
|
||
out.append(s[i])
|
||
i += 1
|
||
return "".join(out)
|
||
|
||
|
||
class Tok:
|
||
def __init__(self, kind: str, value, pos: int):
|
||
self.kind = kind
|
||
self.value = value
|
||
self.pos = pos
|
||
|
||
|
||
def tokenize(src: str) -> list[Tok]:
|
||
toks: list[Tok] = []
|
||
i, n = 0, len(src)
|
||
while i < n:
|
||
c = src[i]
|
||
if c.isspace():
|
||
i += 1
|
||
continue
|
||
if c == '"':
|
||
i += 1
|
||
raw = []
|
||
while i < n:
|
||
ch = src[i]
|
||
if ch == "\\" and i + 1 < n:
|
||
raw.append(ch)
|
||
raw.append(src[i + 1])
|
||
i += 2
|
||
continue
|
||
if ch == '"':
|
||
i += 1
|
||
break
|
||
raw.append(ch)
|
||
i += 1
|
||
while i < n and src[i].isspace():
|
||
i += 1
|
||
while i < n and src[i] == '"':
|
||
i += 1
|
||
while i < n:
|
||
ch = src[i]
|
||
if ch == "\\" and i + 1 < n:
|
||
raw.append(ch)
|
||
raw.append(src[i + 1])
|
||
i += 2
|
||
continue
|
||
if ch == '"':
|
||
i += 1
|
||
break
|
||
raw.append(ch)
|
||
i += 1
|
||
while i < n and src[i].isspace():
|
||
i += 1
|
||
toks.append(Tok("str", unescape("".join(raw)), i))
|
||
continue
|
||
if c in "{}[],;=":
|
||
toks.append(Tok(c, c, i))
|
||
i += 1
|
||
continue
|
||
if c == "-" and i + 1 < n and src[i + 1].isdigit():
|
||
j = i + 1
|
||
while j < n and (src[j].isdigit() or src[j] == "."):
|
||
j += 1
|
||
toks.append(Tok("num", int(float(src[i:j])), i))
|
||
i = j
|
||
continue
|
||
if c.isdigit():
|
||
j = i
|
||
while j < n and (src[j].isdigit() or src[j] == "."):
|
||
j += 1
|
||
text = src[i:j]
|
||
toks.append(Tok("num", int(float(text)) if "." not in text else int(float(text)), i))
|
||
i = j
|
||
continue
|
||
if c == "'" and i + 2 < n:
|
||
j = i + 1
|
||
raw = []
|
||
while j < n and src[j] != "'":
|
||
if src[j] == "\\" and j + 1 < n:
|
||
raw.append(src[j + 1])
|
||
j += 2
|
||
continue
|
||
raw.append(src[j])
|
||
j += 1
|
||
j += 1
|
||
toks.append(Tok("num", ord(raw[0]) if raw else 0, i))
|
||
i = j
|
||
continue
|
||
m = IDENT.match(src, i)
|
||
if m:
|
||
name = m.group(0)
|
||
i = m.end()
|
||
if name == "NULL":
|
||
toks.append(Tok("null", None, i))
|
||
elif name in ("TRUE", "FALSE"):
|
||
toks.append(Tok("num", 1 if name == "TRUE" else 0, i))
|
||
else:
|
||
toks.append(Tok("id", name, i))
|
||
continue
|
||
i += 1
|
||
return toks
|
||
|
||
|
||
def parse_value(toks: list[Tok], i: int):
|
||
if i >= len(toks):
|
||
return None, i
|
||
t = toks[i]
|
||
if t.kind == "str":
|
||
return t.value, i + 1
|
||
if t.kind == "num":
|
||
return t.value, i + 1
|
||
if t.kind == "null":
|
||
return None, i + 1
|
||
if t.kind == "id":
|
||
name = t.value
|
||
i += 1
|
||
if i < len(toks) and toks[i].kind == "(":
|
||
depth = 1
|
||
i += 1
|
||
while i < len(toks) and depth:
|
||
if toks[i].kind == "(":
|
||
depth += 1
|
||
elif toks[i].kind == ")":
|
||
depth -= 1
|
||
i += 1
|
||
return None, i
|
||
return {"$ref": name}, i
|
||
if t.kind == "{":
|
||
return parse_list(toks, i)
|
||
return None, i + 1
|
||
|
||
|
||
def parse_list(toks: list[Tok], i: int):
|
||
assert toks[i].kind == "{"
|
||
i += 1
|
||
items = []
|
||
while i < len(toks) and toks[i].kind != "}":
|
||
if toks[i].kind == ",":
|
||
i += 1
|
||
continue
|
||
val, i = parse_value(toks, i)
|
||
items.append(val)
|
||
if i < len(toks) and toks[i].kind == ",":
|
||
i += 1
|
||
if i < len(toks) and toks[i].kind == "}":
|
||
i += 1
|
||
return items, i
|
||
|
||
|
||
ARRAY_RE = re.compile(
|
||
r"(?:static\s+)?const\s+"
|
||
r"(?:struct\s+)?(?P<type>[A-Za-z_][A-Za-z0-9_]*)"
|
||
r"(?:\s+\*|\s+const\s+\*|\s*)\s*"
|
||
r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)"
|
||
r"(?:\s*\[[^\]]*\])+\s*=\s*\{",
|
||
re.M,
|
||
)
|
||
|
||
CHARPTR_RE = re.compile(
|
||
r"(?:static\s+)?const\s+char\s*\*\s*(?:const\s+)?"
|
||
r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)"
|
||
r"(?:\s*\[[^\]]*\])+\s*=\s*\{",
|
||
re.M,
|
||
)
|
||
|
||
STRING_ASSIGN_RE = re.compile(
|
||
r"(?:static\s+)?const\s+char\s*\*\s*"
|
||
r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?P<body>\"(?:\\.|[^\"\\])*\"(?:\s*\"(?:\\.|[^\"\\])*\")*)\s*;",
|
||
re.S,
|
||
)
|
||
|
||
FUNC_RE = re.compile(
|
||
r"^(?:static\s+)?(?:GtkWidget\s*\*|void|gboolean|int|char\s*\*)\s*"
|
||
r"([A-Za-z_][A-Za-z0-9_]*)\s*\(",
|
||
re.M,
|
||
)
|
||
|
||
|
||
def find_arrays(src: str) -> dict[str, tuple[str, list]]:
|
||
found: dict[str, tuple[str, list]] = {}
|
||
funcs = [(m.start(), m.group(1)) for m in FUNC_RE.finditer(src)]
|
||
|
||
def func_at(pos: int) -> str | None:
|
||
current = None
|
||
for start, name in funcs:
|
||
if start < pos:
|
||
current = name
|
||
else:
|
||
break
|
||
if current is None:
|
||
return None
|
||
# Only treat as local if the array sits after the function's opening brace
|
||
# and before the next top-level function. Good enough for this codebase.
|
||
return current
|
||
|
||
def add(typ: str, name: str, pos_brace: int):
|
||
toks = tokenize(src[pos_brace:])
|
||
if not toks or toks[0].kind != "{":
|
||
return
|
||
val, _ = parse_list(toks, 0)
|
||
fn = func_at(pos_brace)
|
||
key = f"{fn}.{name}" if fn and name in {
|
||
"slides", "qs", "about", "world", "people", "terms", "persons",
|
||
"nouns", "ans",
|
||
} else name
|
||
# Prefer the first global, overwrite locals with prefixed names.
|
||
if key in found and not key.startswith(fn or "\0"):
|
||
return
|
||
found[key] = (typ, val)
|
||
|
||
for m in ARRAY_RE.finditer(src):
|
||
add(m.group("type"), m.group("name"), m.end() - 1)
|
||
for m in CHARPTR_RE.finditer(src):
|
||
add("char*", m.group("name"), m.end() - 1)
|
||
for m in STRING_ASSIGN_RE.finditer(src):
|
||
toks = tokenize(m.group("body"))
|
||
text = "".join(t.value for t in toks if t.kind == "str")
|
||
found[m.group("name")] = ("char", [text])
|
||
return found
|
||
|
||
|
||
def as_list(v):
|
||
return v if isinstance(v, list) else []
|
||
|
||
|
||
def as_str(v) -> str | None:
|
||
if v is None:
|
||
return None
|
||
if isinstance(v, str):
|
||
return v
|
||
return None
|
||
|
||
|
||
def resolve(arrays: dict, val, depth=0):
|
||
if depth > 8:
|
||
return val
|
||
if isinstance(val, dict) and "$ref" in val:
|
||
name = val["$ref"]
|
||
if name in arrays:
|
||
return resolve(arrays, arrays[name][1], depth + 1)
|
||
return None
|
||
if isinstance(val, list):
|
||
return [resolve(arrays, x, depth + 1) for x in val]
|
||
return val
|
||
|
||
|
||
def choice_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 4:
|
||
continue
|
||
opts = [o for o in as_list(row[1]) if isinstance(o, str)]
|
||
correct = row[3] if isinstance(row[3], int) else 0
|
||
out.append({
|
||
"prompt": as_str(row[0]) or "",
|
||
"options": opts,
|
||
"correct": correct,
|
||
})
|
||
return out
|
||
|
||
|
||
def typed_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 2:
|
||
continue
|
||
out.append({
|
||
"prompt": as_str(row[0]) or "",
|
||
"answers": as_str(row[1]) or "",
|
||
"meaning": as_str(row[2]) if len(row) > 2 else None,
|
||
})
|
||
return out
|
||
|
||
|
||
def assembly_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 2:
|
||
continue
|
||
words = [w for w in as_list(row[1]) if isinstance(w, str)]
|
||
n = row[2] if len(row) > 2 and isinstance(row[2], int) else len(words)
|
||
out.append({
|
||
"prompt": as_str(row[0]),
|
||
"words": words[:n],
|
||
})
|
||
return out
|
||
|
||
|
||
def str_list(rows) -> list[str]:
|
||
return [x for x in as_list(rows) if isinstance(x, str)]
|
||
|
||
|
||
def verb_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 3:
|
||
continue
|
||
out.append({
|
||
"before": as_str(row[0]) or "",
|
||
"after": as_str(row[1]) or "",
|
||
"answer": as_str(row[2]) or "",
|
||
})
|
||
return out
|
||
|
||
|
||
def verb_clue_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 4:
|
||
continue
|
||
out.append({
|
||
"emoji": as_str(row[0]) or "",
|
||
"before": as_str(row[1]) or "",
|
||
"after": as_str(row[2]) or "",
|
||
"answer": as_str(row[3]) or "",
|
||
})
|
||
return out
|
||
|
||
|
||
def assign_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 3:
|
||
continue
|
||
out.append({
|
||
"emoji": as_str(row[0]),
|
||
"label": as_str(row[1]) or "",
|
||
"group": row[2] if isinstance(row[2], int) else 0,
|
||
})
|
||
return out
|
||
|
||
|
||
def free_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 4:
|
||
continue
|
||
out.append({
|
||
"question": as_str(row[0]) or "",
|
||
"qCs": as_str(row[1]) or "",
|
||
"sample": as_str(row[2]) or "",
|
||
"aCs": as_str(row[3]) or "",
|
||
})
|
||
return out
|
||
|
||
|
||
def profile_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 2:
|
||
continue
|
||
out.append({
|
||
"stem": as_str(row[0]) or "",
|
||
"sample": as_str(row[1]) or "",
|
||
})
|
||
return out
|
||
|
||
|
||
def slides_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 4:
|
||
continue
|
||
lines = [x for x in as_list(row[3]) if isinstance(x, str)]
|
||
out.append({
|
||
"kicker": as_str(row[0]) or "",
|
||
"title": as_str(row[1]) or "",
|
||
"tip": as_str(row[2]),
|
||
"lines": lines,
|
||
})
|
||
return out
|
||
|
||
|
||
def fill_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 9:
|
||
continue
|
||
segs = [as_str(row[1]), as_str(row[3]), as_str(row[5]), as_str(row[7])]
|
||
ans = [as_str(row[2]), as_str(row[4]), as_str(row[6])]
|
||
out.append({
|
||
"num": as_str(row[0]),
|
||
"segs": [s or "" for s in segs],
|
||
"answers": [a for a in ans if a],
|
||
"mean": as_str(row[8]),
|
||
})
|
||
return out
|
||
|
||
|
||
def let_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 11:
|
||
continue
|
||
segs = [as_str(row[i]) or "" for i in (1, 3, 5, 7, 9)]
|
||
ans = [as_str(row[i]) for i in (2, 4, 6, 8)]
|
||
out.append({
|
||
"num": as_str(row[0]),
|
||
"segs": segs,
|
||
"answers": [a for a in ans if a],
|
||
"mean": as_str(row[10]),
|
||
})
|
||
return out
|
||
|
||
|
||
def ex4row_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 6:
|
||
continue
|
||
segs = [s for s in as_list(row[0]) if s is None or isinstance(s, str)]
|
||
ans = [a for a in as_list(row[1]) if isinstance(a, str)]
|
||
words = [w for w in as_list(row[3]) if isinstance(w, str)] if len(row) > 3 else []
|
||
gmean = [g for g in as_list(row[4]) if isinstance(g, str)] if len(row) > 4 else []
|
||
gaps = row[5] if len(row) > 5 and isinstance(row[5], int) else len(ans)
|
||
out.append({
|
||
"segs": [s or "" for s in segs],
|
||
"answers": ans[:gaps],
|
||
"czech": as_str(row[2]) if len(row) > 2 else None,
|
||
"words": words[:gaps],
|
||
"gmean": gmean[:gaps],
|
||
})
|
||
return out
|
||
|
||
|
||
def kw_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 2:
|
||
continue
|
||
out.append({
|
||
"prompt": as_str(row[0]) or "",
|
||
"answers": as_str(row[1]) or "",
|
||
"mean": as_str(row[2]) if len(row) > 2 else None,
|
||
"german": as_str(row[3]) if len(row) > 3 else None,
|
||
})
|
||
return out
|
||
|
||
|
||
def mluv_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 5:
|
||
continue
|
||
nblank = row[2] if isinstance(row[2], int) else 1
|
||
out.append({
|
||
"prompt": as_str(row[0]) or "",
|
||
"shown": as_str(row[1]) or "",
|
||
"hints": [h for h in as_list(row[3]) if isinstance(h, str)][:nblank],
|
||
"answers": [a for a in as_list(row[4]) if isinstance(a, str)][:nblank],
|
||
})
|
||
return out
|
||
|
||
|
||
def reveal_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 2:
|
||
continue
|
||
out.append({
|
||
"prompt": as_str(row[0]) or "",
|
||
"solution": as_str(row[1]) or "",
|
||
})
|
||
return out
|
||
|
||
|
||
def lit_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 4:
|
||
continue
|
||
opts = [o for o in as_list(row[1]) if isinstance(o, str)]
|
||
out.append({
|
||
"prompt": as_str(row[0]) or "",
|
||
"options": opts,
|
||
"correct": row[2] if isinstance(row[2], int) else 0,
|
||
"expl": as_str(row[3]),
|
||
})
|
||
return out
|
||
|
||
|
||
def trans_sections(rows, arrays) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 2:
|
||
continue
|
||
data = resolve(arrays, row[1])
|
||
out.append({
|
||
"header": as_str(row[0]) or "",
|
||
"rows": typed_items(data),
|
||
})
|
||
return out
|
||
|
||
|
||
def dialogs(arrays) -> list[dict]:
|
||
out = []
|
||
for key in ("ex1_d1", "ex1_d2", "ex1_d3"):
|
||
if key not in arrays:
|
||
continue
|
||
rows = []
|
||
for row in as_list(arrays[key][1]):
|
||
row = as_list(row)
|
||
if len(row) < 4:
|
||
continue
|
||
rows.append({
|
||
"speaker": as_str(row[0]) or "",
|
||
"before": as_str(row[1]) or "",
|
||
"answer": as_str(row[2]) or "",
|
||
"after": as_str(row[3]) or "",
|
||
})
|
||
titles = {"ex1_d1": "Sich vorstellen", "ex1_d2": "Begrüßung", "ex1_d3": "Verabschiedung"}
|
||
out.append({"name": titles[key], "rows": rows})
|
||
return out
|
||
|
||
|
||
def ordne_items(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 3:
|
||
continue
|
||
out.append({
|
||
"mid": as_str(row[0]) or "",
|
||
"fw": as_str(row[1]) or "",
|
||
"ans": as_str(row[2]) or "",
|
||
})
|
||
return out
|
||
|
||
|
||
def ex2_items(arrays) -> list[dict]:
|
||
out = []
|
||
if "ex2_u3_items" not in arrays:
|
||
return out
|
||
for row in as_list(arrays["ex2_u3_items"][1]):
|
||
row = as_list(row)
|
||
if len(row) < 2:
|
||
continue
|
||
raw_rows = resolve(arrays, row[1])
|
||
parsed = []
|
||
for r in as_list(raw_rows):
|
||
r = as_list(r)
|
||
if len(r) < 4:
|
||
continue
|
||
segs = [s or "" for s in as_list(r[1]) if s is None or isinstance(s, str)]
|
||
ans = [a for a in as_list(r[2]) if isinstance(a, str)]
|
||
gaps = r[3] if isinstance(r[3], int) else len(ans)
|
||
parsed.append({
|
||
"num": as_str(r[0]),
|
||
"segs": segs,
|
||
"answers": ans[:gaps],
|
||
})
|
||
out.append({"czech": as_str(row[0]) or "", "rows": parsed})
|
||
return out
|
||
|
||
|
||
def net_ans(rows) -> list[list[dict]]:
|
||
out = []
|
||
for block in as_list(rows):
|
||
block = as_list(block)
|
||
inner = []
|
||
for row in block:
|
||
row = as_list(row)
|
||
if len(row) < 5:
|
||
continue
|
||
inner.append({
|
||
"pfx": row[0] if isinstance(row[0], int) else -1,
|
||
"net": as_str(row[1]),
|
||
"bcast": as_str(row[2]),
|
||
"lo": as_str(row[3]),
|
||
"hi": as_str(row[4]),
|
||
})
|
||
out.append(inner)
|
||
return out
|
||
|
||
|
||
def net_tasks(rows) -> list[dict]:
|
||
out = []
|
||
for row in as_list(rows):
|
||
row = as_list(row)
|
||
if len(row) < 2:
|
||
continue
|
||
out.append({"prompt": as_str(row[0]) or "", "solution": as_str(row[1]) or ""})
|
||
return out
|
||
|
||
|
||
def parse_i18n(src: str) -> dict:
|
||
arrays = find_arrays(src)
|
||
out = {}
|
||
for name in ("tr_ui", "tr_content"):
|
||
if name not in arrays:
|
||
continue
|
||
for row in as_list(arrays[name][1]):
|
||
row = as_list(row)
|
||
if not row or not as_str(row[0]):
|
||
continue
|
||
key = as_str(row[0])
|
||
cs = as_str(row[1]) if len(row) > 1 else None
|
||
en = as_str(row[2]) if len(row) > 2 else None
|
||
if key:
|
||
out[key] = {"cs": cs, "en": en}
|
||
return out
|
||
|
||
|
||
def combo(title, sub, pool, rows, meanings=None, kind="combo", **extra):
|
||
spec = {
|
||
"type": kind,
|
||
"title": title,
|
||
"sub": sub,
|
||
"pool": pool or [],
|
||
"rows": rows,
|
||
"meanings": meanings or [],
|
||
}
|
||
spec.update(extra)
|
||
return spec
|
||
|
||
|
||
def main() -> int:
|
||
arrays: dict[str, tuple[str, list]] = {}
|
||
i18n = {}
|
||
for path in sorted(SRC.glob("*.c")):
|
||
text = strip_comments(path.read_text(encoding="utf-8"))
|
||
arrays.update(find_arrays(text))
|
||
if path.name == "i18n.c":
|
||
i18n = parse_i18n(text)
|
||
|
||
def A(name):
|
||
if name not in arrays:
|
||
return []
|
||
return resolve(arrays, arrays[name][1])
|
||
|
||
german_units = [
|
||
{
|
||
"id": 0,
|
||
"title": "Neue Freunde",
|
||
"page": "unit1",
|
||
"tag": "u1",
|
||
"sub": "unit1_sub",
|
||
"unlocked": True,
|
||
"names": [
|
||
"Dialog", "Sätze bilden", "Was ist richtig?", "Freie Antwort",
|
||
"Zahlen", "Wie viel?", "Zahlenreihe", "Verb einsetzen",
|
||
"Wer? Wie? Wo?", "Wörter trennen", "Grußformen",
|
||
"Was macht er/sie?", "Länder",
|
||
],
|
||
"branch": "u1vocab",
|
||
"exercises": {
|
||
"1": combo("Dialog", "sub_dialog", str_list(A("ex1_pool")),
|
||
[], str_list(A("ex1_meaning")), "dialog",
|
||
dialogues=dialogs(arrays)),
|
||
"2": {
|
||
"type": "assembly", "title": "Sätze bilden", "sub": "sub_assembly",
|
||
"items": assembly_items(A("ex2_items")),
|
||
"meanings": str_list(A("ex2_meaning")),
|
||
},
|
||
"3": {
|
||
"type": "choice", "title": "Was ist richtig?", "sub": "sub_choice_num",
|
||
"questions": choice_items(A("ex3_questions")),
|
||
"meanings": str_list(A("ex3_meaning")),
|
||
},
|
||
"4": {
|
||
"type": "free", "title": "Freie Antwort", "sub": "sub_free",
|
||
"tip": "tip_ss", "questions": free_items(A("ex4_questions")),
|
||
},
|
||
"5": combo("Zahlen", "sub_zahlen", str_list(A("ex5_pool")),
|
||
[{"digits": as_list(r)[0], "answer": as_list(r)[1]}
|
||
for r in as_list(A("ex5_data")) if len(as_list(r)) >= 2],
|
||
str_list(A("ex5_meaning")), "number"),
|
||
"6": combo("Wie viel?", "sub_wieviel", str_list(A("ex6_pool")),
|
||
[{"emoji": as_list(r)[0], "count": as_list(r)[1],
|
||
"noun": as_list(r)[2], "answer": as_list(r)[3]}
|
||
for r in as_list(A("ex6_data")) if len(as_list(r)) >= 4],
|
||
str_list(A("ex6_meaning")), "count"),
|
||
"7": combo("Zahlenreihe", "sub_reihe", str_list(A("ex7_pool")),
|
||
[{"before": as_list(r)[0], "after": as_list(r)[1],
|
||
"answer": as_list(r)[2]}
|
||
for r in as_list(A("ex7_data")) if len(as_list(r)) >= 3],
|
||
str_list(A("ex7_meaning")), "seq"),
|
||
"8": combo("Verb einsetzen", "sub_verb", str_list(A("ex8_pool")),
|
||
verb_items(A("ex8_data")), str_list(A("ex8_meaning")),
|
||
"verb"),
|
||
"9": {
|
||
"type": "choice", "title": "Wer? Wie? Wo?", "sub": "sub_wer",
|
||
"questions": choice_items(A("ex9_questions")),
|
||
"meanings": str_list(A("ex9_meaning")),
|
||
},
|
||
"10": {
|
||
"type": "assembly", "title": "Wörter trennen", "sub": "sub_assembly",
|
||
"items": assembly_items(A("ex10_items")),
|
||
"meanings": str_list(A("ex10_meaning")),
|
||
},
|
||
"11": {
|
||
"type": "assign", "title": "Grußformen", "sub": "sub_gruss",
|
||
"items": assign_items(A("ex11_items")),
|
||
"groups": str_list(A("ex11_groups")),
|
||
"meanings": str_list(A("ex11_meaning")),
|
||
},
|
||
"12": combo("Was macht er/sie?", "sub_bild", str_list(A("ex12_pool")),
|
||
verb_clue_items(A("ex12_data")),
|
||
str_list(A("ex12_meaning")), "verbclue"),
|
||
"13": {
|
||
"type": "assign", "title": "Länder", "sub": "sub_land",
|
||
"items": assign_items(A("ex13_items")),
|
||
"groups": str_list(A("ex13_groups")),
|
||
"meanings": str_list(A("ex13_meaning")),
|
||
},
|
||
},
|
||
"vocab": {
|
||
"type": "vocab", "title": "Vokabeltraining", "sub": "sub_translate",
|
||
"sections": trans_sections(A("u1_trans_sections"), arrays),
|
||
},
|
||
},
|
||
{
|
||
"id": 1,
|
||
"title": "Aus aller Welt",
|
||
"page": "unit2",
|
||
"tag": "u2",
|
||
"sub": "unit2_sub",
|
||
"unlocked": True,
|
||
"names": [
|
||
"Verben konjugieren", "aus oder in", "Fragewörter", "sprechen",
|
||
"Nationalitäten", "Woher?", "Länder schreiben", "Verben einsetzen",
|
||
"Freie Antwort", "Euro", "Wörter suchen", "Was ist richtig?",
|
||
"Ordne zu", "Lückentext", "Verbinde", "Zahlen",
|
||
"Steckbrief", "Berufe", "Nationalität",
|
||
],
|
||
"branch": "u2vocab",
|
||
"exercises": {
|
||
"1": combo("Verben konjugieren", "sub_verben", str_list(A("g01_pool")),
|
||
[], [], "verb_sections",
|
||
sections=[
|
||
{"title": "Peter Fritsch:",
|
||
"rows": verb_items(A("g01_peter")),
|
||
"meanings": str_list(A("g01_peter_mean"))},
|
||
{"title": "Jana Nová und Pavol Korčák:",
|
||
"rows": verb_items(A("g01_jana")),
|
||
"meanings": str_list(A("g01_jana_mean"))},
|
||
],
|
||
transUpfront=True, revealGerman=True),
|
||
"2": {
|
||
"type": "choice", "title": "aus oder in", "sub": "sub_ausin",
|
||
"questions": choice_items(A("g02_rows")),
|
||
"meanings": str_list(A("g02_mean")),
|
||
},
|
||
"3": {
|
||
"type": "choice", "title": "Fragewörter", "sub": "sub_wer",
|
||
"questions": choice_items(A("g03_rows")),
|
||
"meanings": str_list(A("g03_mean")),
|
||
},
|
||
"4": combo("sprechen", "sub_sprich", str_list(A("g04_pool")),
|
||
verb_items(A("g04_rows")), str_list(A("g04_mean")), "verb"),
|
||
"5": {
|
||
"type": "typed", "title": "Nationalitäten", "sub": "sub_nation",
|
||
"rows": typed_items(A("g05_rows")),
|
||
"bank": as_str(A("g05_bank")[0]) if A("g05_bank") else
|
||
(as_str(arrays["g05_bank"][1][0]) if "g05_bank" in arrays
|
||
and arrays["g05_bank"][1] and isinstance(arrays["g05_bank"][1][0], str)
|
||
else None),
|
||
"answersNote": False,
|
||
},
|
||
"6": {
|
||
"type": "typed", "title": "Woher?", "sub": "sub_woher",
|
||
"rows": typed_items(A("g06_rows")),
|
||
},
|
||
"7": {
|
||
"type": "typed", "title": "Länder schreiben", "sub": "sub_laender",
|
||
"rows": typed_items(A("g07_rows")),
|
||
},
|
||
"8": combo("Verben einsetzen", "sub_verb2", str_list(A("g08_pool")),
|
||
verb_items(A("g08_rows")), str_list(A("g08_mean")), "verb"),
|
||
"9": {
|
||
"type": "free", "title": "Freie Antwort", "sub": "sub_free",
|
||
"tip": "tip_ss", "questions": free_items(A("ex9_free_qs")),
|
||
},
|
||
"10": combo("Euro", "sub_euro", str_list(A("g10_pool")),
|
||
verb_items(A("g10_rows")), str_list(A("g10_mean")),
|
||
"verb", preMeaning=True),
|
||
"11": {
|
||
"type": "typed", "title": "Wörter suchen", "sub": "sub_wortsuchen",
|
||
"rows": typed_items(A("g11_rows")),
|
||
},
|
||
"12": {
|
||
"type": "choice", "title": "Was ist richtig?", "sub": "sub_verb2",
|
||
"questions": choice_items(A("g12_rows")),
|
||
"meanings": str_list(A("g12_mean")),
|
||
},
|
||
"13": combo("Ordne zu", "sub_ordne", [],
|
||
ordne_items(A("g13_rows")), str_list(A("g13_mean")),
|
||
"ordne",
|
||
fwPool=str_list(A("g13_fw_pool")),
|
||
ansPool=str_list(A("g13_ans_pool"))),
|
||
"14": combo("Lückentext", "sub_luecke", str_list(A("g14_pool")),
|
||
verb_items(A("g14_rows")), str_list(A("g14_mean")),
|
||
"verb", preMeaning=True, gloss=str_list(A("g14_gloss"))),
|
||
"15": combo("Verbinde", "sub_verbinde", str_list(A("g15_pool")),
|
||
verb_items(A("g15_rows")), str_list(A("g15_mean")),
|
||
"verb", preMeaning=True, gloss=str_list(A("g15_gloss"))),
|
||
"16": combo("Zahlen", "sub_zahlpaar", str_list(A("g16_pool")),
|
||
verb_items(A("g16_rows")), str_list(A("g16_mean")),
|
||
"verb"),
|
||
"17": {
|
||
"type": "profile", "title": "Steckbrief", "sub": "sub_steckbrief",
|
||
"questions": profile_items(A("s01_rows")),
|
||
},
|
||
"18": {
|
||
"type": "hangman", "title": "Berufe", "sub": "sub_berufe",
|
||
"words": str_list(A("hm_words")),
|
||
"tips": str_list(A("hm_tips")),
|
||
"letters": str_list(A("hm_letters")),
|
||
},
|
||
"19": {
|
||
"type": "typed", "title": "Nationalität", "sub": "sub_bistdu",
|
||
"rows": typed_items(A("s03_rows")),
|
||
"answersNote": True,
|
||
},
|
||
},
|
||
"vocab": {
|
||
"type": "vocab", "title": "Vokabeltraining", "sub": "sub_translate",
|
||
"sections": trans_sections(A("u2_trans_sections"), arrays),
|
||
},
|
||
},
|
||
{
|
||
"id": 2,
|
||
"title": "Bei uns zu Hause",
|
||
"page": "unit3",
|
||
"tag": "u3",
|
||
"sub": "unit3_sub",
|
||
"unlocked": True,
|
||
"names": [
|
||
"Familienpaare", "mein oder dein", "ein / kein", "Lückentext",
|
||
"Marcos Familie", "Sortieren", "Possessivtabelle", "Akkusativ",
|
||
"kein / nicht", "Sätze bauen", "Was siehst du?", "Wem gehört das?",
|
||
"Es gibt …", "Beschreiben", "Wochenende",
|
||
],
|
||
"branch": "u3vocab",
|
||
"exercises": {
|
||
"1": combo("Familienpaare", "sub3_pair", str_list(A("ex1_u3_pool")),
|
||
fill_items(A("ex1_u3_rows")), [], "fill",
|
||
sample="Beispiel: der Vater ↔ die Mutter",
|
||
showGerman=False),
|
||
"2": {
|
||
"type": "ex2", "title": "mein oder dein", "sub": "sub3_poss",
|
||
"pool": str_list(A("ex2_u3_pool")),
|
||
"items": ex2_items(arrays),
|
||
},
|
||
"3": combo("ein / kein", "sub3_haustier", str_list(A("ex3_u3_pool")),
|
||
fill_items(A("ex3_u3_rows")), [], "fill",
|
||
sample="Beispiel: Hast du eine Katze? – Nein, ich habe keine Katze. Ich habe einen Hund.",
|
||
showGerman=True),
|
||
"4": {
|
||
"type": "letters_gap", "title": "Lückentext", "sub": "sub3_buchst",
|
||
"rows": ex4row_items(A("ex4_u3_rows")),
|
||
},
|
||
"5": {
|
||
"type": "typed", "title": "Marcos Familie", "sub": "sub3_marco",
|
||
"rows": typed_items(A("ex5_u3_rows")),
|
||
"bank": None,
|
||
},
|
||
"6": {
|
||
"type": "assign", "title": "Sortieren", "sub": "sub3_sort",
|
||
"items": assign_items(A("ex6_u3_items")),
|
||
"groups": str_list(A("ex6_u3_groups")),
|
||
"meanings": str_list(A("ex6_u3_meaning")),
|
||
},
|
||
"7": {
|
||
"type": "table", "title": "Possessivtabelle", "sub": "sub3_tabelle",
|
||
"persons": [
|
||
"ich", "du", "er", "sie", "es", "wir", "ihr", "sie / Sie",
|
||
],
|
||
"nouns": ["der Garten", "die Idee", "das Fest", "die Geschwister"],
|
||
"answers": [
|
||
["mein", "dein", "sein", "ihr", "sein", "unser", "euer", "ihr"],
|
||
["meine", "deine", "seine", "ihre", "seine", "unsere", "eure", "ihre"],
|
||
["mein", "dein", "sein", "ihr", "sein", "unser", "euer", "ihr"],
|
||
["meine", "deine", "seine", "ihre", "seine", "unsere", "eure", "ihre"],
|
||
],
|
||
},
|
||
"8": combo("Akkusativ", "sub3_akk", str_list(A("ex8_u3_pool")),
|
||
fill_items(A("ex8_u3_rows")), str_list(A("ex8_u3_gloss")),
|
||
"fill", showGerman=True),
|
||
"9": combo("kein / nicht", "sub3_nicht", str_list(A("ex9_u3_pool")),
|
||
fill_items(A("ex9_u3_rows")), [], "fill", showGerman=True),
|
||
"10": {
|
||
"type": "assembly", "title": "Sätze bauen", "sub": "sub3_satz",
|
||
"items": assembly_items(A("ex10_u3_items")),
|
||
"meanings": str_list(A("ex10_u3_meaning")),
|
||
},
|
||
"11": {
|
||
"type": "kw", "title": "Was siehst du?", "sub": "sub3_sehen",
|
||
"sample": "Beispiel: Ich sehe eine Burg. / Ich sehe ein Schloss.",
|
||
"rows": kw_items(A("ex11_u3_qs")),
|
||
},
|
||
"12": combo("Wem gehört das?", "sub3_wem", str_list(A("ex12_u3_pool")),
|
||
fill_items(A("ex12_u3_rows")), [], "fill", showGerman=True),
|
||
"13": combo("Es gibt …", "sub3_gibt", str_list(A("ex13_u3_pool")),
|
||
fill_items(A("ex13_u3_rows")), [], "fill", showGerman=True),
|
||
"14": {
|
||
"type": "kw", "title": "Beschreiben", "sub": "sub3_saetze",
|
||
"sample": "Beispiel: Ich sehe einen Jungen. Er liest ein Buch.",
|
||
"rows": kw_items(A("ex14_u3_qs")),
|
||
},
|
||
"15": {
|
||
"type": "letters", "title": "Wochenende", "sub": "sub3_wochen",
|
||
"rows": let_items(A("ex15_u3_rows")),
|
||
},
|
||
},
|
||
"vocab": {
|
||
"type": "vocab", "title": "Vokabeltraining", "sub": "sub_translate",
|
||
"sections": trans_sections(A("u3_trans_sections"), arrays),
|
||
},
|
||
},
|
||
]
|
||
|
||
locked_german = [
|
||
"Schule und Freizeit", "Guten Appetit!", "Mein Tagesablauf",
|
||
"Meine Freunde", "Wir treffen uns in Salzburg",
|
||
"Mein Haus ist meine Burg", "Urlaub in Österreich",
|
||
]
|
||
for i, title in enumerate(locked_german, start=3):
|
||
german_units.append({
|
||
"id": i, "title": title, "page": None, "tag": None,
|
||
"sub": None, "unlocked": False, "names": [],
|
||
"exercises": {},
|
||
})
|
||
|
||
# g05_bank is a single string, not an array of strings
|
||
if "g05_bank" in arrays:
|
||
raw = arrays["g05_bank"][1]
|
||
bank = None
|
||
if isinstance(raw, list) and raw and isinstance(raw[0], str) and len(raw) == 1:
|
||
bank = raw[0]
|
||
elif isinstance(raw, str):
|
||
bank = raw
|
||
if bank:
|
||
german_units[1]["exercises"]["5"]["bank"] = bank
|
||
|
||
if "ex5_u3_bank" in arrays:
|
||
raw = arrays["ex5_u3_bank"][1]
|
||
if isinstance(raw, list) and raw and isinstance(raw[0], str):
|
||
german_units[2]["exercises"]["5"]["bank"] = raw[0] if len(raw) == 1 else " ".join(
|
||
x for x in raw if isinstance(x, str)
|
||
)
|
||
|
||
net_lessons = []
|
||
for i in range(1, 28):
|
||
slides = slides_items(A(f"build_net_unit{i}_page.slides"))
|
||
qs = choice_items(A(f"net{i}_qs")) if i > 1 else []
|
||
net_lessons.append({
|
||
"id": i,
|
||
"titleKey": f"net_unit{i}",
|
||
"subKey": f"net_unit{i}_sub",
|
||
"exTitleKey": f"net_ex{i}_title" if i > 1 else "net_ex_title",
|
||
"quizHeadKey": f"net_quiz{i}_head" if i > 1 else None,
|
||
"slides": slides,
|
||
"quiz": qs,
|
||
})
|
||
|
||
hw_lessons = []
|
||
for i in range(1, 4):
|
||
hw_lessons.append({
|
||
"id": i,
|
||
"titleKey": f"hw_unit{i}",
|
||
"subKey": f"hw_unit{i}_sub",
|
||
"exTitleKey": f"hw_ex{i}_title",
|
||
"quizHeadKey": f"hw_quiz{i}_head",
|
||
"slides": slides_items(A(f"build_hw_unit{i}_page.slides")),
|
||
"quiz": choice_items(A(f"build_hw_unit{i}_exercise_page.qs")),
|
||
})
|
||
|
||
mluv_meta = [
|
||
(1, "typed", "Pravopis – i/y po obojetných souhláskách",
|
||
"Doplňte i/í nebo y/ý.", None, "e1_items"),
|
||
(2, "choice", "Pravopis – velká/malá písmena",
|
||
"Vyberte správnou variantu.", None, "e2_qs"),
|
||
(3, "typed", "Slovní druhy",
|
||
"Určete slovní druh podtržených slov.",
|
||
"Do pole napište název slovního druhu.", "e3_items"),
|
||
(4, "typed", "Mluvnické kategorie – podstatná jména",
|
||
"Určete rod, číslo a pád podtržených slov.",
|
||
"Vyplňte tři pole: rod, číslo a pád.", "e4_items"),
|
||
(5, "typed", "Mluvnické kategorie – slovesa",
|
||
"Určete osobu, číslo, čas a způsob u těchto sloves.",
|
||
"U rozkazovacího a podmiňovacího způsobu napište do pole čas „neurčitý“.",
|
||
"e5_items"),
|
||
(6, "reveal", "Slovotvorba", "Rozeberte slova na morfémy.",
|
||
"Určete předponu, kořen, příponu a koncovku.", "e6_items"),
|
||
(7, "typed", "Větné členy",
|
||
"Určete větný člen podtržených výrazů.", None, "e7_items"),
|
||
(8, "typed", "Druhy vedlejších vět",
|
||
"Určete druh vedlejší věty.", None, "e8_items"),
|
||
(9, "typed", "Synonyma", "Ke každému slovu napište synonymum.", None, "e9_items"),
|
||
(10, "typed", "Antonyma", "Napište opak.", None, "e10_items"),
|
||
(11, "typed", "Pravopis – s/z na začátku slova",
|
||
"Vyberte správnou předponu s-/z-.", None, "e11_items"),
|
||
(12, "reveal", "Skladba – najděte chybu",
|
||
"Každá věta obsahuje jednu chybu. Najděte ji a opravte.", None, "e12_items"),
|
||
(13, "typed", "Tvarosloví – správný tvar",
|
||
"Dejte slovo do správného tvaru.", None, "e13_items"),
|
||
(14, "choice", "Výběr ze čtyř možností",
|
||
"Vyberte správnou odpověď.", None, "e14_qs"),
|
||
(15, "reveal", "Přímá a nepřímá řeč",
|
||
"Přepište přímou řeč na nepřímou.", None, "e15_items"),
|
||
(16, "reveal", "Slovní zásoba – rozdíly",
|
||
"Vysvětlete rozdíl mezi těmito dvojicemi slov.", None, "e16_items"),
|
||
(17, "reveal", "Interpunkce",
|
||
"Doplňte čárky tam, kde patří.", None, "e17_items"),
|
||
(18, "typed", "Obrazná pojmenování",
|
||
"Určete, o jaký druh obrazného pojmenování jde.", None, "e18_items"),
|
||
(19, "typed", "Stylistika – slohové útvary",
|
||
"Přiřaďte ukázku ke správnému slohovému útvaru.", None, "e19_items"),
|
||
(20, "choice", "Souhrnné opakování",
|
||
"Vyberte správnou odpověď.", None, "e20_qs"),
|
||
]
|
||
mluv_names = [
|
||
"i/y", "Velká písmena", "Slovní druhy", "Podst. jména", "Slovesa",
|
||
"Slovotvorba", "Větné členy", "Vedlejší věty", "Synonyma", "Antonyma",
|
||
"s/z", "Najdi chybu", "Tvary", "Test A–D", "Přímá řeč",
|
||
"Rozdíly", "Interpunkce", "Obrazná pojmen.", "Slohové útvary",
|
||
"Opakování",
|
||
]
|
||
mluvnice = []
|
||
for n, kind, title, sub, note, key in mluv_meta:
|
||
item = {"id": n, "name": mluv_names[n - 1], "type": kind,
|
||
"title": title, "sub": sub, "note": note}
|
||
if kind == "typed":
|
||
item["items"] = mluv_items(A(key))
|
||
elif kind == "reveal":
|
||
item["items"] = reveal_items(A(key))
|
||
else:
|
||
item["questions"] = choice_items(A(key))
|
||
expl_key = key.replace("_qs", "_expl")
|
||
if expl_key in arrays:
|
||
item["expls"] = str_list(A(expl_key))
|
||
mluvnice.append(item)
|
||
|
||
def note_lines(fn, name):
|
||
return str_list(A(f"{fn}.{name}"))
|
||
|
||
books = [
|
||
{
|
||
"id": "1984",
|
||
"title": "George Orwell – 1984",
|
||
"subKey": "cetba1984_sub",
|
||
"page": "cetba1984",
|
||
"quizTitle": "lit_quiz_title",
|
||
"quizSub": "lit_quiz_sub",
|
||
"plotTitle": "lit_plot_title",
|
||
"plotSub": "lit_plot_sub",
|
||
"notes": [
|
||
{"title": "O knize", "icon": "book",
|
||
"lines": note_lines("build_cetba1984_page", "about")},
|
||
{"title": "Svět a strana", "icon": "globe",
|
||
"lines": note_lines("build_cetba1984_page", "world")},
|
||
{"title": "Postavy", "icon": "people",
|
||
"lines": note_lines("build_cetba1984_page", "people")},
|
||
{"title": "Klíčové pojmy", "icon": "bulb",
|
||
"lines": note_lines("build_cetba1984_page", "terms")},
|
||
],
|
||
"quiz": lit_items(A("lit_qs")),
|
||
"plot": assembly_items(A("lit_plot_items")),
|
||
"plotMeaning": str_list(A("lit_plot_meaning")),
|
||
},
|
||
{
|
||
"id": "fuks",
|
||
"title": "Ladislav Fuks – Spalovač mrtvol",
|
||
"subKey": "cetbaFuks_sub",
|
||
"page": "cetbaFuks",
|
||
"quizTitle": "lit_fuks_quiz_title",
|
||
"quizSub": "lit_fuks_quiz_sub",
|
||
"plotTitle": "lit_fuks_plot_title",
|
||
"plotSub": "lit_fuks_plot_sub",
|
||
"notes": [
|
||
{"title": "O knize", "icon": "book",
|
||
"lines": note_lines("build_cetba_fuks_page", "about")},
|
||
{"title": "Doba a svět", "icon": "globe",
|
||
"lines": note_lines("build_cetba_fuks_page", "world")},
|
||
{"title": "Postavy", "icon": "people",
|
||
"lines": note_lines("build_cetba_fuks_page", "people")},
|
||
{"title": "Klíčové motivy", "icon": "bulb",
|
||
"lines": note_lines("build_cetba_fuks_page", "terms")},
|
||
],
|
||
"quiz": lit_items(A("fuks_qs")),
|
||
"plot": assembly_items(A("fuks_plot_items")),
|
||
"plotMeaning": str_list(A("fuks_plot_meaning")),
|
||
},
|
||
]
|
||
|
||
subjects = [
|
||
{"key": "Deutsch", "open": True, "target": "roadmap", "icon": "de"},
|
||
{"key": "Správa počítačových sítí", "open": True, "target": "netyears", "icon": "wifi"},
|
||
{"key": "Technické vybavení", "open": True, "target": "hwmap", "icon": "chip"},
|
||
{"key": "Český jazyk a literatura", "open": True, "target": "czechmap", "icon": "cz"},
|
||
{"key": "Občanská nauka", "open": False, "target": None, "icon": "lock"},
|
||
{"key": "English", "open": False, "target": None, "icon": "lock"},
|
||
{"key": "Matematika", "open": False, "target": None, "icon": "lock"},
|
||
{"key": "Fyzika", "open": False, "target": None, "icon": "lock"},
|
||
{"key": "Základy Přírodopisných věd", "open": False, "target": None, "icon": "lock"},
|
||
{"key": "Technická grafika", "open": False, "target": None, "icon": "lock"},
|
||
{"key": "Prezentační grafika", "open": False, "target": None, "icon": "lock"},
|
||
{"key": "Programování", "open": False, "target": None, "icon": "lock"},
|
||
{"key": "Programové vybavení", "open": False, "target": None, "icon": "lock"},
|
||
]
|
||
|
||
payload = {
|
||
"i18n": i18n,
|
||
"subjects": subjects,
|
||
"german": german_units,
|
||
"net": {
|
||
"lessons": net_lessons,
|
||
"tasks": net_tasks(A("net_tasks")),
|
||
"answers": net_ans(A("net_ans")),
|
||
},
|
||
"hw": hw_lessons,
|
||
"mluvnice": mluvnice,
|
||
"books": books,
|
||
}
|
||
|
||
# Fix single-string banks stored as char* "arrays"
|
||
for key in ("g05_bank", "ex5_u3_bank"):
|
||
if key not in arrays:
|
||
continue
|
||
raw = arrays[key][1]
|
||
if isinstance(raw, list) and len(raw) == 1 and isinstance(raw[0], str) and " " in raw[0]:
|
||
if key == "g05_bank":
|
||
german_units[1]["exercises"]["5"]["bank"] = raw[0]
|
||
else:
|
||
german_units[2]["exercises"]["5"]["bank"] = raw[0]
|
||
|
||
text = json.dumps(payload, ensure_ascii=False, indent=2)
|
||
for dest in OUTS:
|
||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||
dest.write_text(text, encoding="utf-8")
|
||
|
||
def count(path, pred):
|
||
node = payload
|
||
for p in path:
|
||
node = node[p]
|
||
return sum(1 for x in node if pred(x))
|
||
|
||
print("wrote " + ", ".join(str(dest) for dest in OUTS))
|
||
print(f" i18n keys: {len(i18n)}")
|
||
print(f" german unlocked exercises: "
|
||
f"{sum(len(u.get('exercises', {})) for u in german_units if u['unlocked'])}")
|
||
print(f" net lessons with slides: {sum(1 for L in net_lessons if L['slides'])}")
|
||
print(f" net quizzes: {sum(1 for L in net_lessons if L['quiz'])}")
|
||
print(f" hw lessons: {sum(1 for L in hw_lessons if L['slides'])}")
|
||
print(f" mluvnice: {len(mluvnice)}")
|
||
print(f" books quiz items: {[len(b['quiz']) for b in books]}")
|
||
missing = []
|
||
for u in german_units:
|
||
if not u["unlocked"]:
|
||
continue
|
||
for k, ex in u["exercises"].items():
|
||
t = ex["type"]
|
||
if t == "choice" and not ex.get("questions"):
|
||
missing.append(f"{u['tag']}e{k} choice")
|
||
if t == "typed" and not ex.get("rows"):
|
||
missing.append(f"{u['tag']}e{k} typed")
|
||
if t == "assembly" and not ex.get("items"):
|
||
missing.append(f"{u['tag']}e{k} assembly")
|
||
if t == "vocab" and not ex.get("sections"):
|
||
missing.append(f"{u['tag']} vocab")
|
||
if t == "fill" and not ex.get("rows"):
|
||
missing.append(f"{u['tag']}e{k} fill")
|
||
if t == "hangman" and not ex.get("words"):
|
||
missing.append(f"{u['tag']}e{k} hangman")
|
||
if u.get("vocab") and not u["vocab"].get("sections"):
|
||
missing.append(f"{u['tag']} vocab sections")
|
||
for L in net_lessons:
|
||
if not L["slides"]:
|
||
missing.append(f"net{L['id']} slides")
|
||
if L["id"] > 1 and not L["quiz"]:
|
||
missing.append(f"net{L['id']} quiz")
|
||
for L in hw_lessons:
|
||
if not L["slides"]:
|
||
missing.append(f"hw{L['id']} slides")
|
||
if not L["quiz"]:
|
||
missing.append(f"hw{L['id']} quiz")
|
||
for m in mluvnice:
|
||
if m["type"] in ("typed", "reveal") and not m.get("items"):
|
||
missing.append(f"mluv{m['id']}")
|
||
if m["type"] == "choice" and not m.get("questions"):
|
||
missing.append(f"mluv{m['id']}")
|
||
if missing:
|
||
print("MISSING:")
|
||
for m in missing:
|
||
print(" ", m)
|
||
return 1
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|