From 3596ce80d4057984935e9222a353a54b2f5fad9b Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 22 Feb 2026 23:49:45 +0100 Subject: [PATCH 1/9] feature: create zip, fixes #9 --- main.py | 5 +++++ src/build.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/main.py b/main.py index a53294a..ef5b04b 100644 --- a/main.py +++ b/main.py @@ -23,6 +23,7 @@ def main( output: Annotated[str | None, typer.Option("--output", "-o", help="Destination directory to build to.")] = None, render_editions: Annotated[bool, typer.Option(help="Skip building editions.")] = True, render_solutions: Annotated[bool, typer.Option(help="Skip building solutions.")] = True, + build_zip: Annotated[bool, typer.Option(help="Whether to build a zip file of the output.")] = False, ) -> None: conf = config.Config(content_path=input, build_path=output) bd = build.Build(conf) @@ -42,6 +43,10 @@ def main( # Copy assets to build folder bd.build_assets() + if build_zip: + # Build zip file of output + bd.build_zip() + if __name__ == "__main__": app() diff --git a/src/build.py b/src/build.py index 6d56a6f..64f71d4 100644 --- a/src/build.py +++ b/src/build.py @@ -1,7 +1,9 @@ import json +import os import random import re import shutil +import zipfile from pathlib import Path from jinja2 import Environment, FileSystemLoader @@ -515,3 +517,39 @@ 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 build_zip(self, zip_name: str | None = None) -> Path: + """Create a zip archive of the complete build output directory. + + The zip file is written into the build directory itself and is excluded + from the archive contents. + """ + + build_dir = self.config.p_build + build_dir.mkdir(parents=True, exist_ok=True) + + zip_path = build_dir / (zip_name if zip_name is not None else f"{build_dir.name}.zip") + zip_path = zip_path.resolve() + + # If a previous archive exists, remove it first to avoid zipping stale data. + if zip_path.exists(): + zip_path.unlink() + + tqdm.write(f"Creating zip archive: {zip_path}") + + with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: + # os.walk includes dotfiles; sort for stable archives. + for root, dirs, files in os.walk(build_dir): + dirs.sort() + files.sort() + + root_path = Path(root) + for filename in files: + file_path = (root_path / filename).resolve() + if file_path == zip_path: + continue + + arcname = file_path.relative_to(build_dir.resolve()) + zf.write(file_path, arcname=arcname) + + return zip_path From da3cfade6a056658fa87d8fe9806bf32b88718d8 Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Mon, 23 Feb 2026 00:10:42 +0100 Subject: [PATCH 2/9] fix: add zip also to build.py --- build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/build.py b/build.py index 5caae9f..7646086 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_zip() From 5f6b0e76823a33c1fbc7002ad71e58d92838c6ac Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sat, 28 Feb 2026 23:39:47 +0100 Subject: [PATCH 3/9] feature: add also decoder and demapper --- latex/settings.tex | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/latex/settings.tex b/latex/settings.tex index 1310043..4bfe1e3 100644 --- a/latex/settings.tex +++ b/latex/settings.tex @@ -1087,6 +1087,28 @@ \pgfcirc@activate@bipole@simple{l}{channelcoder} \makeatother +% Channel Decoder +\makeatletter +\ctikzset{bipoles/channeldecoder/width/.initial=.7} + +\pgfcirc@define@twoports{blocks} +{} +{\ctikzvalof{bipoles/channeldecoder/width}} +{channeldecoder} +{\ctikzvalof{bipoles/channeldecoder/width}} +{\ctikzvalof{bipoles/channeldecoder/width}} +{ + \pgfcirc@twoport@draw@splitline + % + \pgf@circ@text@strokecolor + \pgftext[center,x=0.25\pgf@circ@res@left,y=0.6\pgf@circ@res@up]{\tiny 01+01} + \pgftext[center,x=0.55\pgf@circ@res@right,y=0.6\pgf@circ@res@down]{\tiny 01} +} + +\pgfcirc@activate@bipole@simple{l}{channeldecoder} +\makeatother + + % Source Coder \makeatletter \ctikzset{bipoles/sourcecoder/width/.initial=.7} @@ -1108,6 +1130,27 @@ \pgfcirc@activate@bipole@simple{l}{sourcecoder} \makeatother +% Source Decoder +\makeatletter +\ctikzset{bipoles/sourcedecoder/width/.initial=.7} + +\pgfcirc@define@twoports{blocks} +{} +{\ctikzvalof{bipoles/sourcedecoder/width}} +{sourcedecoder} +{\ctikzvalof{bipoles/sourcedecoder/width}} +{\ctikzvalof{bipoles/sourcedecoder/width}} +{ + \pgfcirc@twoport@draw@splitline + % + \pgf@circ@text@strokecolor + \pgftext[center,x=0.55\pgf@circ@res@left,y=0.6\pgf@circ@res@up]{\tiny 01} + \pgftext[center,x=0.30\pgf@circ@res@right,y=0.6\pgf@circ@res@down]{\tiny 0101} +} + +\pgfcirc@activate@bipole@simple{l}{sourcedecoder} +\makeatother + % Mapper \makeatletter \ctikzset{bipoles/mapper/width/.initial=.7} @@ -1147,4 +1190,45 @@ } \pgfcirc@activate@bipole@simple{l}{mapper} +\makeatother + +% Demapper +\makeatletter +\ctikzset{bipoles/demapper/width/.initial=.7} + +\pgfcirc@define@twoports{blocks} +{} +{\ctikzvalof{bipoles/demapper/width}} +{demapper} +{\ctikzvalof{bipoles/demapper/width}} +{\ctikzvalof{bipoles/demapper/width}} +{ + \pgfcirc@twoport@draw@splitline + % + \pgf@circ@text@strokecolor + % + \pgfpathmoveto{\pgfpoint{0.50\pgf@circ@res@left}{+0.25\pgf@circ@res@up}} + \pgfpathlineto{\pgfpoint{0.50\pgf@circ@res@left}{+0.75\pgf@circ@res@up}} + \pgfusepath{draw} + % + \pgfpathmoveto{\pgfpoint{0.25\pgf@circ@res@left}{+0.50\pgf@circ@res@up}} + \pgfpathlineto{\pgfpoint{0.75\pgf@circ@res@left}{+0.50\pgf@circ@res@up}} + \pgfusepath{draw} + % + \pgfpathcircle{\pgfpoint{0.65\pgf@circ@res@left}{+0.65\pgf@circ@res@up}}{0.05\pgf@circ@res@up} + \pgfusepath{fill} + % + \pgfpathcircle{\pgfpoint{0.35\pgf@circ@res@left}{+0.65\pgf@circ@res@up}}{0.05\pgf@circ@res@up} + \pgfusepath{fill} + % + \pgfpathcircle{\pgfpoint{0.65\pgf@circ@res@left}{+0.35\pgf@circ@res@up}}{0.05\pgf@circ@res@up} + \pgfusepath{fill} + % + \pgfpathcircle{\pgfpoint{0.35\pgf@circ@res@left}{+0.35\pgf@circ@res@up}}{0.05\pgf@circ@res@up} + \pgfusepath{fill} + % + \pgftext[center,x=0.55\pgf@circ@res@right,y=-0.6\pgf@circ@res@up]{\tiny 01} +} + +\pgfcirc@activate@bipole@simple{l}{demapper} \makeatother \ No newline at end of file From 1e361787f039e09c0d7d6c94dcdd36d995865c71 Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Wed, 1 Apr 2026 23:30:43 +0200 Subject: [PATCH 4/9] feature: implement index on html side as stub --- renderer/fifty_ohm_html_renderer.py | 6 ++++++ renderer/index.py | 16 ++++++++++++++++ test/test_index.py | 19 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 renderer/index.py create mode 100644 test/test_index.py diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py index c73a511..2e316bd 100644 --- a/renderer/fifty_ohm_html_renderer.py +++ b/renderer/fifty_ohm_html_renderer.py @@ -6,6 +6,7 @@ from .dash import Dash from .formula import Formula from .halfwidth_spaces import HalfwidthSpaces from .include import Include +from .index import Index from .morse import Morse from .nonbreaking_spaces import NonbreakingSpaces, NonbreakingSpacesDots from .photo import Photo @@ -64,6 +65,7 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): Qso, Include, Formula, + Index, *extras, **kwargs, ) @@ -332,3 +334,7 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): def render_formula(self, token): return f"\n$${token.formula}$$\n" + + def render_index(self, token): + # For HTML just ignore the index + return "" diff --git a/renderer/index.py b/renderer/index.py new file mode 100644 index 0000000..6885b47 --- /dev/null +++ b/renderer/index.py @@ -0,0 +1,16 @@ +import re + +from mistletoe.span_token import SpanToken + + +class Index(SpanToken): + """ + Resolve index commands + Identifies "[index:term:subterm]" or "[index:term]" + """ + + pattern = re.compile(r"[ \t]*\[index:([^:\]\s]+)(?::([^:\]\s]+))?\]") + + def __init__(self, match_object): + self.first = match_object.group(1) + self.second = match_object.group(2) diff --git a/test/test_index.py b/test/test_index.py new file mode 100644 index 0000000..446f076 --- /dev/null +++ b/test/test_index.py @@ -0,0 +1,19 @@ +import mistletoe +import pytest + +from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer +from test.util import paragraph + + +@pytest.mark.html +def test_index_html(): + + assertions = { + "Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", + "Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", + "Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", + "Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", + } + + for key, value in assertions.items(): + assert mistletoe.markdown(key, FiftyOhmHtmlRenderer) == paragraph(value) From 850fbce913ffb0ec44499f353ac5f4fee36cc18c Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Wed, 1 Apr 2026 23:36:48 +0200 Subject: [PATCH 5/9] feature: index in LaTeX --- renderer/fifty_ohm_latex_renderer.py | 8 ++++++++ test/test_index.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/renderer/fifty_ohm_latex_renderer.py b/renderer/fifty_ohm_latex_renderer.py index 508983e..86989ca 100644 --- a/renderer/fifty_ohm_latex_renderer.py +++ b/renderer/fifty_ohm_latex_renderer.py @@ -3,6 +3,7 @@ from mistletoe.latex_renderer import LaTeXRenderer from .comment import BlockComment from .dash import Dash from .halfwidth_spaces import HalfwidthSpaces +from .index import Index from .nonbreaking_spaces import NonbreakingSpaces, NonbreakingSpacesDots from .photo import Photo from .picture import Picture @@ -32,6 +33,7 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer): TableRow, TableHeader, TableCell, + Index, ) self.question_renderer = question_renderer @@ -148,3 +150,9 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer): def render_table_cell(self, token: TableCell): return self.render_inner(token) + + def render_index(self, token): + if token.second: + return f"\\index{{{token.first}!{token.second}}}" + else: + return f"\\index{{{token.first}}}" diff --git a/test/test_index.py b/test/test_index.py index 446f076..7adafef 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -2,6 +2,7 @@ import mistletoe import pytest from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer +from renderer.fifty_ohm_latex_renderer import FiftyOhmLaTeXRenderer from test.util import paragraph @@ -17,3 +18,17 @@ def test_index_html(): for key, value in assertions.items(): assert mistletoe.markdown(key, FiftyOhmHtmlRenderer) == paragraph(value) + + +@pytest.mark.latex +def test_index_latex(): + + assertions = { + "Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne!Yagi-Antenne} oft gedreht", + "Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne!Yagi-Antenne} oft gedreht", + "Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne} oft gedreht", + "Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne} oft gedreht", + } + + for key, value in assertions.items(): + assert mistletoe.markdown(key, FiftyOhmLaTeXRenderer) == "\n" + value + "\n" From e250a39e1446f40014ae3b21537d2aef5daf80fc Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Wed, 1 Apr 2026 23:48:57 +0200 Subject: [PATCH 6/9] fix: tests on github --- test/test_index.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/test_index.py b/test/test_index.py index 7adafef..e0d3015 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -10,8 +10,8 @@ from test.util import paragraph def test_index_html(): assertions = { - "Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", - "Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", + "Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501 + "Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501 "Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", "Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", } @@ -24,10 +24,10 @@ def test_index_html(): def test_index_latex(): assertions = { - "Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne!Yagi-Antenne} oft gedreht", - "Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne!Yagi-Antenne} oft gedreht", - "Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne} oft gedreht", - "Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne} oft gedreht", + "Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne!Yagi-Antenne} oft gedreht", # noqa: E501 + "Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne!Yagi-Antenne} oft gedreht", # noqa: E501 + "Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne} oft gedreht", # noqa: E501 + "Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne} oft gedreht", # noqa: E501 } for key, value in assertions.items(): From ceab6a15bb473862a326bfc5d8550880a6e87a5f Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Wed, 1 Apr 2026 23:56:34 +0200 Subject: [PATCH 7/9] fix: remove empty lines --- test/test_index.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test_index.py b/test/test_index.py index e0d3015..e978fd8 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -8,7 +8,6 @@ from test.util import paragraph @pytest.mark.html def test_index_html(): - assertions = { "Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501 "Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501 @@ -22,7 +21,6 @@ def test_index_html(): @pytest.mark.latex def test_index_latex(): - assertions = { "Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne!Yagi-Antenne} oft gedreht", # noqa: E501 "Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne!Yagi-Antenne} oft gedreht", # noqa: E501 From bce6add76bc9267490ce0ef13b4988832bb57e62 Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 5 Apr 2026 12:15:00 +0200 Subject: [PATCH 8/9] fix: improved regex --- renderer/index.py | 2 +- test/test_index.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/renderer/index.py b/renderer/index.py index 6885b47..606dc55 100644 --- a/renderer/index.py +++ b/renderer/index.py @@ -9,7 +9,7 @@ class Index(SpanToken): Identifies "[index:term:subterm]" or "[index:term]" """ - pattern = re.compile(r"[ \t]*\[index:([^:\]\s]+)(?::([^:\]\s]+))?\]") + pattern = re.compile(r"[ \t]*\[index:([^:\]\s](?:[^:\]\s]| )*)(?::([^:\]\s](?:[^:\]\s]| )*))?\]") def __init__(self, match_object): self.first = match_object.group(1) diff --git a/test/test_index.py b/test/test_index.py index e978fd8..769fc65 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -13,6 +13,7 @@ def test_index_html(): "Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501 "Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", "Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", + "Im Betrieb werden Yagi-Antennen[index:Test Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", } for key, value in assertions.items(): @@ -26,6 +27,7 @@ def test_index_latex(): "Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne!Yagi-Antenne} oft gedreht", # noqa: E501 "Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne} oft gedreht", # noqa: E501 "Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne} oft gedreht", # noqa: E501 + "Im Betrieb werden Yagi-Antennen [index:Test Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Test Antenne} oft gedreht", # noqa: E501 } for key, value in assertions.items(): From 3faf276315ca77895c77f3ea4574938f70e8d2c9 Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 5 Apr 2026 12:48:47 +0200 Subject: [PATCH 9/9] fix: issue with # noqa: E501 --- test/test_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_index.py b/test/test_index.py index 769fc65..2c2e74b 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -13,7 +13,7 @@ def test_index_html(): "Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501 "Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", "Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", - "Im Betrieb werden Yagi-Antennen[index:Test Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", + "Im Betrieb werden Yagi-Antennen[index:Test Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501 } for key, value in assertions.items():