From 9f0d0ffc3a981b99ff6c8faa430797c2e2e73099 Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 9 Nov 2025 08:42:29 +0100 Subject: [PATCH 01/11] feature: download svgs instead of building them --- build.py | 2 +- src/download.py | 50 ++++++++++--------------------------------------- 2 files changed, 11 insertions(+), 41 deletions(-) diff --git a/build.py b/build.py index e5143dc..2cd7c8f 100644 --- a/build.py +++ b/build.py @@ -14,8 +14,8 @@ dl.download_edition("N") # Download additional files: dl.download_question_metadata() -dl.download_pictures() dl.download_photos() +dl.download_pictures() dl.download_includes() dl.download_snippets() dl.download_content() diff --git a/src/download.py b/src/download.py index 9459417..301d69f 100644 --- a/src/download.py +++ b/src/download.py @@ -1,9 +1,5 @@ import json -import multiprocessing -import os -import shutil -from joblib import Memory, Parallel, delayed from tqdm import tqdm from api.directus import DirectusAPI @@ -97,50 +93,24 @@ class Download: for photo in tqdm(photos, desc="Downloading photos"): data = self.api.get_file("assets/" + photo["photo"]) - with (self.config.p_data_photos / f"{photo['id']}.jpg").open("wb") as file: + with (self.config.p_data_photos / f"{photo["id"]}.jpg").open("wb") as file: file.write(data) def download_pictures(self): self.config.p_data_pictures.mkdir(parents=True, exist_ok=True) - # Symlink photos, so LaTeX can access these assets to render into graphics. - foto_link = self.config.p_data_pictures / "foto" - if not foto_link.is_symlink(): - foto_link.symlink_to(self.config.p_data_photos) - memory = Memory("./cache", verbose=0) + pictures = self.api.get("items/pictures", { "limit": -1 }) - @memory.cache - def build_picture(picture): - pid = picture["id"] + for picture in tqdm(pictures, desc="Downloading pictures") : - if ( - "\\begin{document}" in picture["latex"] or "\\begin{table}" in picture["latex"] - ): # Workaround, if the picture contains a document class it should be ignored! - return + # Download LaTeX Sources: + with (self.config.p_data_pictures / f"{picture["id"]}include.tex").open("w") as file: + file.write(picture["latex"]) - if self.config.no_latex: - shutil.copyfile("./assets/images/50ohm_gray.svg", self.config.p_data_pictures / f"{pid}.svg") - else: - with (self.config.p_data_pictures / f"img/{pid}include.tex").open("w") as file: - file.write(picture["latex"]) - - aux_file = self.config.p_data_photos / f"{pid}.tex" - with aux_file.open("w", encoding="utf-8") as file: - # Write the auxilary LaTeX file: - file.write(f"\\documentclass{{BNetzA-Fragenkatalog}}\\DARCimageOnly{{9cm}}{{{pid}include}}") - # Build the picture: - os.system(f"latexmk -lualatex -f -interaction=nonstopmode {aux_file} > /dev/null 2>&1") - # Convert the PDF to SVG: - os.system( - "pdftocairo -svg" - f"{self.config.p_data_pictures / f'{pid}.pdf'} - > {self.config.p_data_pictures / f'{pid}.svg'}" - ) - - pictures = self.api.get("items/pictures", params={"limit": -1}) - - Parallel(n_jobs=multiprocessing.cpu_count())( - delayed(build_picture)(picture) for picture in tqdm(pictures, desc="Build All Pictures") - ) + # Download SVGs: + data = self.api.get_file(f"assets/{picture["picture"]}") + with (self.config.p_data_pictures / f"{picture["id"]}.svg").open("wb") as file: + file.write(data) def download_includes(self): includes = self.api.get("items/include", params={"limit": -1}) From a203e2ffe7871fdcb71125161c842cb5ec8ccfd9 Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 9 Nov 2025 10:18:40 +0100 Subject: [PATCH 02/11] feature: add amazon link to index --- templates/html/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/html/index.html b/templates/html/index.html index 1ded1eb..ffee0a5 100644 --- a/templates/html/index.html +++ b/templates/html/index.html @@ -135,6 +135,7 @@

Die Lernhinhalte für die Klasse N gibt es auch als gedrucktes Buch. Es ist passend zum Online-Kurs ausgelegt.

From f72879967c2297b3fba90d2191386bf5c6f121cb Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Sun, 9 Nov 2025 10:33:56 +0100 Subject: [PATCH 03/11] feat: improve table rendering --- assets/style-course.css | 5 -- renderer/fifty_ohm_html_renderer.py | 46 +++++----- renderer/table.py | 128 ++++++++++++++++++---------- 3 files changed, 111 insertions(+), 68 deletions(-) diff --git a/assets/style-course.css b/assets/style-course.css index ff5703b..d74c881 100644 --- a/assets/style-course.css +++ b/assets/style-course.css @@ -2,11 +2,6 @@ margin-bottom: 2ex; } -.table_inner { - padding: 1ex; - background: #ddd; -} - .table tr { border-bottom: 1px solid #aaa; } diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py index 9e00d84..b5ffa28 100644 --- a/renderer/fifty_ohm_html_renderer.py +++ b/renderer/fifty_ohm_html_renderer.py @@ -13,7 +13,7 @@ from .qso import Qso from .question import Question from .quote import Quote from .references import References -from .table import Table +from .table import Table, TableBody, TableCell, TableHeader, TableRow from .tag import Tag from .underline import Underline from .unit import Unit @@ -46,6 +46,10 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): Picture, Photo, Table, + TableHeader, + TableRow, + TableCell, + TableBody, Qso, Include, *extras, @@ -104,7 +108,7 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): for char in morse_code: result += '\n' for symbol in char: - result += '\n' + result += '\n' if symbol == 1: result += "▄" elif symbol == 2: @@ -217,28 +221,32 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): self.photo_handler(token.id) return self.render_photo_helper(token.id, token.ref, token.text, token.number) - @staticmethod - def render_cell_helper(cell, alignment, type): - style = "" - if alignment in table_alignment: - style = f' style="text-align: {table_alignment[alignment]};"' - return f"<{type}{style}>{cell}\n" - - def render_table(self, token): - alignment = token.alignment - table = "\n" - - for i, row in enumerate(token.children): - content = "" - for j, cell in enumerate(row.children): - content += self.render_cell_helper(self.render_inner(cell), alignment[j], ("th" if i == 0 else "td")) - table += f"\n{content}\n" + def render_table(self, token: Table): + table = f'
\n{self.render_inner(token)}' if token.caption != "": - table += f"" + table += f"\n" + table += "
{token.caption}{token.caption}
" return table + def render_table_row(self, token: TableRow): + return f"\n{self.render_inner(token)}\n" + + def render_table_header(self, token: TableHeader): + return self.render_table_row(token) + + def render_table_cell(self, token: TableCell): + type = "th" if token.header else "td" + style = "" + if token.alignment in table_alignment: + style = f' style="text-align: {table_alignment[token.alignment]};"' + return f"<{type}{style}>{self.render_inner(token)}\n" + + def render_table_body(self, token: TableBody): + type = "thead" if token.header else "tbody" + return f"<{type}>\n{self.render_inner(token)}\n" + def render_include(self, token): return self.include_handler(token.ident) diff --git a/renderer/table.py b/renderer/table.py index 93b1c23..21966f3 100644 --- a/renderer/table.py +++ b/renderer/table.py @@ -1,37 +1,99 @@ import re +from enum import Enum from mistletoe import span_token from mistletoe.block_token import BlockToken +class CellAlignment(Enum): + LEFT = "l" + CENTER = "c" + RIGHT = "r" + EXPAND = "X" + + +class TableCell(BlockToken): + alignment: CellAlignment + header: bool + + @staticmethod + def start(_): + return False + + def __init__(self, match, alignment: CellAlignment, header=False): + super().__init__(match, span_token.tokenize_inner) + self.alignment, self.header = alignment, header + + +class TableRow(BlockToken): + @staticmethod + def start(_): + return False + + @classmethod + def parse(cls, line) -> list[str]: + line = line.strip().strip("|") + return [col.strip() for col in line.split("|")] + + def __init__(self, match, alignment: list[CellAlignment]): + self.children = [TableCell(cell, alignment[i]) for i, cell in enumerate(self.parse(match))] + + +class TableHeader(TableRow): + alignment: list[CellAlignment] + + @staticmethod + def start(_): + return False + + @classmethod + def parse(cls, line) -> list[str]: + alignment: list[CellAlignment] = [] + columns: list[str] = [] + for column in super().parse(line): + attr: CellAlignment + value: str + attr, value = re.match(r"^ ?(?:([lcrX]):)? ?(.*)", column).group(1, 2) + + alignment.append(attr) + columns.append(value) + + return columns, alignment + + def __init__(self, match): + columns, self.alignment = self.parse(match) + self.children = [TableCell(cell, self.alignment[i], True) for i, cell in enumerate(columns)] + + +class TableBody(BlockToken): + header: bool + + @staticmethod + def start(_): + return False + + def __init__(self, rows: TableRow, header=False): + self.children, self.header = rows, header + + class Table(BlockToken): + name: str + caption: str + header: list[TableHeader] + rows: list[TableRow] + @staticmethod def start(line): return "|" in line - @classmethod - def parse_header(cls, line): - # Parse header, extract alignment and make an ordinary row out of it - line = line.strip().strip("|") # Remove leading/trailing whitespace and pipes - columns = [col.strip() for col in line.split("|")] # Split by pipe | - - alignment = [] - header = "| " - - for column in columns: - attr, val = re.match(r"^ ?(?:([lcrX]):)?(.*)", column).group(1, 2) - alignment.append(attr) - header += val + " | " - - return header, alignment - @classmethod def read(cls, lines): - header = next(lines) - header, alignment = cls.parse_header(header) + header = TableHeader(next(lines)) + alignment = header.alignment + name = "" caption = "" - rows = [TableRow(header)] + children = [TableBody([header], True)] # Read table until the end: No more column definitions, caption, or empty line. line_buffer = [] @@ -48,31 +110,9 @@ class Table(BlockToken): line_buffer.append(next(lines)) next_line = lines.peek() - rows.extend([TableRow(line) for line in line_buffer]) + children.append(TableBody([TableRow(line, alignment) for line in line_buffer])) - return alignment, rows, name, caption + return children, name, caption def __init__(self, match): - self.alignment, self.children, self.name, self.caption = match - - -class TableRow(BlockToken): - @classmethod - def parse_row(cls, line): - line = line.strip().strip("|") - columns = [col.strip() for col in line.split("|")] - - data = [] - - for column in columns: - data.append(TableCell(column)) - - return data - - def __init__(self, match): - self.children = self.parse_row(match) - - -class TableCell(BlockToken): - def __init__(self, match): - super().__init__(match, span_token.tokenize_inner) + self.children, self.name, self.caption = match From 0df7d575953be0fce007b9c7857c1d59cf7f41de Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 9 Nov 2025 10:43:44 +0100 Subject: [PATCH 04/11] feature: enable auto format on save --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8decbe7..28b391b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "[python]": { + "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.ruff": "explicit", "source.organizeImports.ruff": "explicit" From bf94317945a7487cf6fe3a55fb46b02333b0155f Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 9 Nov 2025 10:44:07 +0100 Subject: [PATCH 05/11] fix: format to pass ci --- src/download.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/download.py b/src/download.py index 301d69f..a0926ef 100644 --- a/src/download.py +++ b/src/download.py @@ -93,23 +93,22 @@ class Download: for photo in tqdm(photos, desc="Downloading photos"): data = self.api.get_file("assets/" + photo["photo"]) - with (self.config.p_data_photos / f"{photo["id"]}.jpg").open("wb") as file: + with (self.config.p_data_photos / f"{photo['id']}.jpg").open("wb") as file: file.write(data) def download_pictures(self): self.config.p_data_pictures.mkdir(parents=True, exist_ok=True) - pictures = self.api.get("items/pictures", { "limit": -1 }) - - for picture in tqdm(pictures, desc="Downloading pictures") : + pictures = self.api.get("items/pictures", {"limit": -1}) + for picture in tqdm(pictures, desc="Downloading pictures"): # Download LaTeX Sources: - with (self.config.p_data_pictures / f"{picture["id"]}include.tex").open("w") as file: + with (self.config.p_data_pictures / f"{picture['id']}include.tex").open("w") as file: file.write(picture["latex"]) # Download SVGs: - data = self.api.get_file(f"assets/{picture["picture"]}") - with (self.config.p_data_pictures / f"{picture["id"]}.svg").open("wb") as file: + data = self.api.get_file(f"assets/{picture['picture']}") + with (self.config.p_data_pictures / f"{picture['id']}.svg").open("wb") as file: file.write(data) def download_includes(self): From 339966c5e53dedaf763dd8500fd52ab64e89ff41 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Sun, 9 Nov 2025 10:59:57 +0100 Subject: [PATCH 06/11] docs: update comments --- src/download.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/download.py b/src/download.py index a0926ef..4c9d586 100644 --- a/src/download.py +++ b/src/download.py @@ -102,11 +102,11 @@ class Download: pictures = self.api.get("items/pictures", {"limit": -1}) for picture in tqdm(pictures, desc="Downloading pictures"): - # Download LaTeX Sources: + # Save LaTeX sources to file with (self.config.p_data_pictures / f"{picture['id']}include.tex").open("w") as file: file.write(picture["latex"]) - # Download SVGs: + # Download SVG data = self.api.get_file(f"assets/{picture['picture']}") with (self.config.p_data_pictures / f"{picture['id']}.svg").open("wb") as file: file.write(data) From 42ba32cc11d78410db76979f194aa177fc9be8f4 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Sun, 9 Nov 2025 11:46:46 +0100 Subject: [PATCH 07/11] feat: render new tables in LaTeX --- renderer/fifty_ohm_latex_renderer.py | 42 +++++++++++++++++----------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/renderer/fifty_ohm_latex_renderer.py b/renderer/fifty_ohm_latex_renderer.py index 21a9883..508983e 100644 --- a/renderer/fifty_ohm_latex_renderer.py +++ b/renderer/fifty_ohm_latex_renderer.py @@ -8,7 +8,7 @@ from .photo import Photo from .picture import Picture from .question import Question from .quote import Quote -from .table import Table +from .table import Table, TableBody, TableCell, TableHeader, TableRow from .tag import Tag from .underline import Underline @@ -28,9 +28,17 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer): Picture, Photo, Table, + TableBody, + TableRow, + TableHeader, + TableCell, ) self.question_renderer = question_renderer + def render_inner(self, token, base="") -> str: + # Filter out None values, so block tokens can return None to not be rendered. + return base.join(filter(lambda x: x is not None, [self.render(child) for child in token.children])) + def render_document(self, token): self.footnotes.update(token.footnotes) return self.render_inner(token) @@ -118,23 +126,25 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer): return self.render_photo_helper(token.id, token.ref, token.text, token.number) def render_table(self, token): - alignments = token.alignment - align = "" - for alignment in alignments: - align += alignment - - table = f"\\begin{{DARCtabular}}{{{align}}}\n" - - for row in token.children: - for j, cell in enumerate(row.children): - table += self.render_inner(cell) - if j < len(row.children) - 1: - table += " & " - table += r"\\" + "\n" - - table += "\\end{DARCtabular}" + table = "\\begin{DARCtabular}" + f"{self.render_inner(token)}" + "\\end{DARCtabular}" if token.caption != "": table += f"\\captionof{{figure}}{{{token.caption}}}\n" table += f"\\label{{{token.name}}}\n" return table + + def render_table_body(self, token: TableBody): + return self.render_inner(token) + + def render_table_row(self, token: TableRow): + return rf"{self.render_inner(token, ' & ')}\\" + "\n" + + def render_table_header(self, token: TableHeader): + align = "" + for alignment in token.alignment: + align += alignment + + return f"{{{align}}}\n{self.render_table_row(token)}" + + def render_table_cell(self, token: TableCell): + return self.render_inner(token) From 03cbfae60cd577143dddb7896d7ee690934d2353 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Sun, 9 Nov 2025 11:59:48 +0100 Subject: [PATCH 08/11] fix: only render table bodies with children --- renderer/fifty_ohm_html_renderer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py index b5ffa28..8cb9e66 100644 --- a/renderer/fifty_ohm_html_renderer.py +++ b/renderer/fifty_ohm_html_renderer.py @@ -245,8 +245,11 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): return f"<{type}{style}>{self.render_inner(token)}\n" def render_table_body(self, token: TableBody): - type = "thead" if token.header else "tbody" - return f"<{type}>\n{self.render_inner(token)}\n" + if token.children is None or len(token.children) == 0: + return None + else: + type = "thead" if token.header else "tbody" + return f"<{type}>\n{self.render_inner(token)}\n" def render_include(self, token): return self.include_handler(token.ident) From f685faa010221749615e5e8ade9c7975cdf09291 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Sun, 9 Nov 2025 12:00:03 +0100 Subject: [PATCH 09/11] fix: update unit tests with new table code --- test/test_table.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/test_table.py b/test/test_table.py index deaba13..1f37ffe 100644 --- a/test/test_table.py +++ b/test/test_table.py @@ -8,17 +8,17 @@ from renderer.fifty_ohm_latex_renderer import FiftyOhmLaTeXRenderer @pytest.mark.html def test_table_html(): assertions = { - "| l: a |": '\n\n\n\n
a
\n', - "| r: a |": '\n\n\n\n
a
\n', - "| c: a |": '\n\n\n\n
a
\n', - "| l: a |\n| b |": '\n\n\n\n\n\n\n
a
b
\n', # noqa: E501 - "| l: a |\n| b |\n| c |": '\n\n\n\n\n\n\n\n\n\n
a
b
c
\n', # noqa: E501 - "| a |\n| b |\n| c |": "\n\n\n\n\n\n\n\n\n\n
a
b
c
\n", # noqa: E501 - "| l: a |\n| *b* |": '\n\n\n\n\n\n\n
a
b
\n', # noqa: E501 - "| l: a |\n| b |\n[table:n_ab:A und B]": '\n\n\n\n\n\n\n
a
b
A und B
\n', # noqa: E501 - "| l: *a* |": '\n\n\n\n
a
\n', - "| l: a |\n| [morse:a] |": '\n\n\n\n\n\n\n
a
\n\n▄\n\n▄▄▄\n\n
\n', # noqa: E501 - "| l: abc | r: abc | X: abc |\n| 123 | 456 | 789 | \n | 987 | 654 | 321 | ": '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
abcabcabc
123456789
987654321
\n', # noqa: E501 + "| l: a |": '\n\n\n\n\n\n
a
\n', # noqa: E501 + "| r: a |": '\n\n\n\n\n\n
a
\n', # noqa: E501 + "| c: a |": '\n\n\n\n\n\n
a
\n', # noqa: E501 + "| l: a |\n| b |": '\n\n\n\n\n\n\n\n\n\n\n
a
b
\n', # noqa: E501 + "| l: a |\n| b |\n| c |": '\n\n\n\n\n\n\n\n\n\n\n\n\n\n
a
b
c
\n', # noqa: E501 + "| a |\n| b |\n| c |": '\n\n\n\n\n\n\n\n\n\n\n\n\n\n
a
b
c
\n', # noqa: E501 + "| l: a |\n| *b* |": '\n\n\n\n\n\n\n\n\n\n\n
a
b
\n', # noqa: E501 + "| l: a |\n| b |\n[table:n_ab:A und B]": '\n\n\n\n\n\n\n\n\n\n\n\n
a
b
A und B
\n', # noqa: E501 + "| l: *a* |": '\n\n\n\n\n\n
a
\n', # noqa: E501 + "| l: a |\n| [morse:a] |": '\n\n\n\n\n\n\n\n\n\n\n
a
\n\n▄\n\n▄▄▄\n\n
\n', # noqa: E501 + "| l: abc | r: abc | X: abc |\n| 123 | 456 | 789 | \n | 987 | 654 | 321 | ": '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
abcabcabc
123456789
987654321
\n', # noqa: E501 } with FiftyOhmHtmlRenderer() as renderer: From a6345913e1a3af80e5847f46ef9a96b9012384b6 Mon Sep 17 00:00:00 2001 From: Matthias Jung Date: Sun, 9 Nov 2025 15:48:23 +0100 Subject: [PATCH 10/11] refactor: updated chats --- templates/html/index.html | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/templates/html/index.html b/templates/html/index.html index ffee0a5..02e4da6 100644 --- a/templates/html/index.html +++ b/templates/html/index.html @@ -100,11 +100,9 @@

DARC Chat
-

Der DARC e. V. betreibt eine eigene Matrix-Instanz. Dort gibt es Räume, in denen du deine Fragen stellen kannst (🌍 öffentlich, d. h. auch von anderen Matrix-Servern erreichbar, und 🔐 nur für Mitglieder):

+

Der DARC e. V. betreibt eine eigene Matrix-Instanz. Dort gibt es einen Raum, in denen du deine Fragen stellen kannst:


Mitglied werden
@@ -142,7 +140,7 @@
-

Kurse vor Ort

+

Kurse

Einige unserer Ortsverbände bieten Präsenz- oder Onlinekurse mit erfahrenen Ausbildern an. Finde heraus, ob es einen für dich passenden Kurs bei dir in der Nähe gibt!

Präsenz From 0ee0445bd47ecae09d488e8ffce270e133853aa2 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Wed, 12 Nov 2025 12:50:02 +0100 Subject: [PATCH 11/11] fix: apply list styling to course lists --- assets/style-course.css | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/assets/style-course.css b/assets/style-course.css index d74c881..7a34f8b 100644 --- a/assets/style-course.css +++ b/assets/style-course.css @@ -170,16 +170,8 @@ td { text-align: justify; } -ul.list { - list-style: none; /* Remove default bullets */ -} - -ul.list li::before { - content: "▪"; +ul li::marker { color: #2aa6da; - display: inline-block; - width: 1em; - margin-left: -1em; } ol li { @@ -237,10 +229,10 @@ span.formula { @media (min-width: 800px) { .course p, + .course ul, + .course ol, .course .question, - .course .table, - .course ul.list, - .course ol.list { + .course .table { width: 59.3%; } @@ -257,8 +249,8 @@ span.formula { } .course .margin p, - .course .margin ul.list, - .course .margin ol.list { + .course .margin ul, + .course .margin ol { width: auto; }