From 191495d6ce8f3d802944dd7c968647015567ef9c Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 5 Apr 2026 15:20:16 +0200 Subject: [PATCH 1/9] feature: extract question index --- build.py | 1 + main.py | 1 + src/build.py | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/build.py b/build.py index 5caae9f..27061e6 100644 --- a/build.py +++ b/build.py @@ -14,3 +14,4 @@ bd.build_edition("EA") bd.build_edition("NEA") bd.build_assets() bd.build_solutions() +bd.build_question_index() diff --git a/main.py b/main.py index a53294a..6574521 100644 --- a/main.py +++ b/main.py @@ -41,6 +41,7 @@ def main( # Copy assets to build folder bd.build_assets() + bd.build_question_index() if __name__ == "__main__": diff --git a/src/build.py b/src/build.py index 6d56a6f..b544fcf 100644 --- a/src/build.py +++ b/src/build.py @@ -2,6 +2,7 @@ import json import random import re import shutil +from collections import defaultdict from pathlib import Path from jinja2 import Environment, FileSystemLoader @@ -23,6 +24,9 @@ class Build: self.env.filters["shuffle_answers"] = self.__filter_shuffle_answers self.questions = self.__parse_katalog() + self.question_index = defaultdict(set) + self.question_token_pattern = re.compile(r"^\s*\[question:([\w\d]+)\]", re.MULTILINE) + def __parse_katalog(self): with self.config.p_data_fragenkatalog.open() as file: fragenkatalog = json.load(file) @@ -389,6 +393,9 @@ class Build: section_content = sfile.read() section["slide"] = section_content + if section["content"] is not None: + self.__collect_question_links(edition, ident, section["content"]) + next_section = ( chapter["sections"][section_number] if section_number < len(chapter["sections"]) else None ) @@ -515,3 +522,20 @@ class Build: page = solution_template.render(question=question, solution=solution, number=solution_file.stem) page = self.__build_page(page, course_wrapper=False) file.write(page) + + def __collect_question_links(self, edition: str, section_ident: str, section_content: str): + section_filename = f"{edition}_{section_ident}.html" + + for question_number in self.question_token_pattern.findall(section_content or ""): + self.question_index[question_number].add(f"{section_filename}#{question_number}") + + def __serialize_question_index(self) -> dict[str, list[str]]: + serialized: dict[str, list[str]] = {} + for question, links in sorted(self.question_index.items()): + serialized[question] = sorted(links) + return serialized + + def build_question_index(self): + with (self.config.p_build / "question_index.json").open("w", encoding="utf-8") as file: + json.dump(self.__serialize_question_index(), file, ensure_ascii=False, indent=2, sort_keys=True) + file.write("\n") From fe8c82cc6e055cf79d588f9eba1561782958aa6d Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 5 Apr 2026 15:27:35 +0200 Subject: [PATCH 2/9] refactor: changed question_index layout --- src/build.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/build.py b/src/build.py index b544fcf..7d1ec63 100644 --- a/src/build.py +++ b/src/build.py @@ -24,7 +24,7 @@ class Build: self.env.filters["shuffle_answers"] = self.__filter_shuffle_answers self.questions = self.__parse_katalog() - self.question_index = defaultdict(set) + self.question_index = defaultdict(lambda: defaultdict(set)) self.question_token_pattern = re.compile(r"^\s*\[question:([\w\d]+)\]", re.MULTILINE) def __parse_katalog(self): @@ -394,7 +394,7 @@ class Build: section["slide"] = section_content if section["content"] is not None: - self.__collect_question_links(edition, ident, section["content"]) + self.__collect_question_occurrences(edition, ident, section["content"]) next_section = ( chapter["sections"][section_number] if section_number < len(chapter["sections"]) else None @@ -523,16 +523,20 @@ class Build: page = self.__build_page(page, course_wrapper=False) file.write(page) - def __collect_question_links(self, edition: str, section_ident: str, section_content: str): - section_filename = f"{edition}_{section_ident}.html" - + def __collect_question_occurrences(self, edition: str, section_ident: str, section_content: str): for question_number in self.question_token_pattern.findall(section_content or ""): - self.question_index[question_number].add(f"{section_filename}#{question_number}") + self.question_index[question_number][section_ident].add(edition) - def __serialize_question_index(self) -> dict[str, list[str]]: - serialized: dict[str, list[str]] = {} - for question, links in sorted(self.question_index.items()): - serialized[question] = sorted(links) + def __serialize_question_index(self) -> dict[str, list[dict[str, list[str]]]]: + serialized: dict[str, list[dict[str, list[str]]]] = {} + for question, sections in sorted(self.question_index.items()): + serialized[question] = [ + { + "section": section, + "editions": sorted(editions), + } + for section, editions in sorted(sections.items()) + ] return serialized def build_question_index(self): From b1ee348d4768875d3b70e69836243a87a867fc22 Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 5 Apr 2026 15:44:46 +0200 Subject: [PATCH 3/9] feature: add search link and search page build --- src/build.py | 1 + templates/html/page.html | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/build.py b/src/build.py index 7d1ec63..d14e434 100644 --- a/src/build.py +++ b/src/build.py @@ -502,6 +502,7 @@ class Build: self.__build_course_page(snippets, "patenkarte", "patenkarte") self.__build_html_page(contents, "pruefung") self.__build_html_page(contents, "infos") + self.__build_html_page(contents, "suche") def build_solutions(self): for solution_file in self.config.p_data_solutions.glob("*.md"): diff --git a/templates/html/page.html b/templates/html/page.html index 9cfb40d..24cf4c0 100644 --- a/templates/html/page.html +++ b/templates/html/page.html @@ -66,6 +66,12 @@ Lernen +
  • + + search + Suche + +
  • info From 54ce67d95fa77adb6819f60e8d52251fffd64c90 Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 5 Apr 2026 16:07:24 +0200 Subject: [PATCH 4/9] feature: add section title to question index --- src/build.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/build.py b/src/build.py index d14e434..65cf7f7 100644 --- a/src/build.py +++ b/src/build.py @@ -24,7 +24,7 @@ class Build: self.env.filters["shuffle_answers"] = self.__filter_shuffle_answers self.questions = self.__parse_katalog() - self.question_index = defaultdict(lambda: defaultdict(set)) + self.question_index = defaultdict(dict) self.question_token_pattern = re.compile(r"^\s*\[question:([\w\d]+)\]", re.MULTILINE) def __parse_katalog(self): @@ -394,7 +394,7 @@ class Build: section["slide"] = section_content if section["content"] is not None: - self.__collect_question_occurrences(edition, ident, section["content"]) + self.__collect_question_occurrences(edition, ident, section["title"], section["content"]) next_section = ( chapter["sections"][section_number] if section_number < len(chapter["sections"]) else None @@ -524,19 +524,33 @@ class Build: page = self.__build_page(page, course_wrapper=False) file.write(page) - def __collect_question_occurrences(self, edition: str, section_ident: str, section_content: str): + def __collect_question_occurrences( + self, + edition: str, + section_ident: str, + section_title: str, + section_content: str, + ): for question_number in self.question_token_pattern.findall(section_content or ""): - self.question_index[question_number][section_ident].add(edition) + section_entry = self.question_index[question_number].setdefault( + section_ident, + { + "section_title": section_title, + "editions": set(), + }, + ) + section_entry["editions"].add(edition) - def __serialize_question_index(self) -> dict[str, list[dict[str, list[str]]]]: - serialized: dict[str, list[dict[str, list[str]]]] = {} + def __serialize_question_index(self) -> dict[str, list[dict[str, object]]]: + serialized: dict[str, list[dict[str, object]]] = {} for question, sections in sorted(self.question_index.items()): serialized[question] = [ { "section": section, - "editions": sorted(editions), + "section_title": data["section_title"], + "editions": sorted(data["editions"]), } - for section, editions in sorted(sections.items()) + for section, data in sorted(sections.items()) ] return serialized From e0ac81e33505a5497c506af138560a2dfe26a17d Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 5 Apr 2026 16:24:40 +0200 Subject: [PATCH 5/9] feature: add chapter title to question index --- src/build.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/build.py b/src/build.py index 65cf7f7..21d5136 100644 --- a/src/build.py +++ b/src/build.py @@ -394,7 +394,13 @@ class Build: section["slide"] = section_content if section["content"] is not None: - self.__collect_question_occurrences(edition, ident, section["title"], section["content"]) + self.__collect_question_occurrences( + edition, + chapter["title"], + ident, + section["title"], + section["content"], + ) next_section = ( chapter["sections"][section_number] if section_number < len(chapter["sections"]) else None @@ -527,6 +533,7 @@ class Build: def __collect_question_occurrences( self, edition: str, + chapter_title: str, section_ident: str, section_title: str, section_content: str, @@ -535,6 +542,7 @@ class Build: section_entry = self.question_index[question_number].setdefault( section_ident, { + "chapter_title": chapter_title, "section_title": section_title, "editions": set(), }, @@ -547,6 +555,7 @@ class Build: serialized[question] = [ { "section": section, + "chapter_title": data["chapter_title"], "section_title": data["section_title"], "editions": sorted(data["editions"]), } From f445c6ab8f3e10d75dbd30cf1794b76fad838179 Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 5 Apr 2026 16:50:37 +0200 Subject: [PATCH 6/9] refactor: build question index in assets --- src/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build.py b/src/build.py index 21d5136..f115449 100644 --- a/src/build.py +++ b/src/build.py @@ -564,6 +564,6 @@ class Build: return serialized def build_question_index(self): - with (self.config.p_build / "question_index.json").open("w", encoding="utf-8") as file: + with (self.p_build_assets / "question_index.json").open("w", encoding="utf-8") as file: json.dump(self.__serialize_question_index(), file, ensure_ascii=False, indent=2, sort_keys=True) file.write("\n") From bec3012edbf943a6a08ecb940fc5096b43192c7e Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 5 Apr 2026 16:52:33 +0200 Subject: [PATCH 7/9] fix: asset path --- src/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build.py b/src/build.py index f115449..cbe57e8 100644 --- a/src/build.py +++ b/src/build.py @@ -564,6 +564,6 @@ class Build: return serialized def build_question_index(self): - with (self.p_build_assets / "question_index.json").open("w", encoding="utf-8") as file: + with (self.config.p_build_assets / "question_index.json").open("w", encoding="utf-8") as file: json.dump(self.__serialize_question_index(), file, ensure_ascii=False, indent=2, sort_keys=True) file.write("\n") From a3b21beae6b63ac2016211500be7a658c6e960f9 Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 5 Apr 2026 22:43:57 +0200 Subject: [PATCH 8/9] feature: add solution to question index --- src/build.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/build.py b/src/build.py index cbe57e8..0fbb083 100644 --- a/src/build.py +++ b/src/build.py @@ -2,7 +2,6 @@ import json import random import re import shutil -from collections import defaultdict from pathlib import Path from jinja2 import Environment, FileSystemLoader @@ -24,7 +23,7 @@ class Build: self.env.filters["shuffle_answers"] = self.__filter_shuffle_answers self.questions = self.__parse_katalog() - self.question_index = defaultdict(dict) + self.question_index = {} self.question_token_pattern = re.compile(r"^\s*\[question:([\w\d]+)\]", re.MULTILINE) def __parse_katalog(self): @@ -539,31 +538,26 @@ class Build: section_content: str, ): for question_number in self.question_token_pattern.findall(section_content or ""): - section_entry = self.question_index[question_number].setdefault( - section_ident, + question_entry = self.question_index.setdefault( + question_number, { + "has_solution": self.__has_solution(question_number), + "section": section_ident, "chapter_title": chapter_title, "section_title": section_title, - "editions": set(), + "editions": [], }, ) - section_entry["editions"].add(edition) + if edition not in question_entry["editions"]: + question_entry["editions"].append(edition) - def __serialize_question_index(self) -> dict[str, list[dict[str, object]]]: - serialized: dict[str, list[dict[str, object]]] = {} - for question, sections in sorted(self.question_index.items()): - serialized[question] = [ - { - "section": section, - "chapter_title": data["chapter_title"], - "section_title": data["section_title"], - "editions": sorted(data["editions"]), - } - for section, data in sorted(sections.items()) - ] - return serialized + def __has_solution(self, question_number: str) -> bool: + return (self.config.p_data_solutions / f"{question_number}.md").exists() def build_question_index(self): + for question_data in self.question_index.values(): + question_data["editions"] = sorted(question_data["editions"]) + with (self.config.p_build_assets / "question_index.json").open("w", encoding="utf-8") as file: - json.dump(self.__serialize_question_index(), file, ensure_ascii=False, indent=2, sort_keys=True) + json.dump(self.question_index, file, ensure_ascii=False, indent=2, sort_keys=True) file.write("\n") From 5ca687b8d648a281dc72cdbffff91df3560273fb Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 5 Apr 2026 23:19:28 +0200 Subject: [PATCH 9/9] fix: ruff checks --- src/build.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/build.py b/src/build.py index 9d2d9de..b3edc00 100644 --- a/src/build.py +++ b/src/build.py @@ -560,9 +560,11 @@ class Build: for question_data in self.question_index.values(): question_data["editions"] = sorted(question_data["editions"]) - with (self.config.p_build_assets / "question_index.json").open("w", encoding="utf-8") as file: + path = self.config.p_build_assets / "question_index.json" + with (path).open("w", encoding="utf-8") as file: json.dump(self.question_index, file, ensure_ascii=False, indent=2, sort_keys=True) file.write("\n") + def build_zip(self, zip_name: str | None = None) -> Path: """Create a zip archive of the complete build output directory.