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" diff --git a/assets/style-course.css b/assets/style-course.css index 7b9a2cf..5aa6ca7 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; } @@ -175,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 { @@ -252,10 +239,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%; } @@ -272,8 +259,8 @@ span.formula { } .course .margin p, - .course .margin ul.list, - .course .margin ol.list { + .course .margin ul, + .course .margin ol { width: auto; } 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/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py index 9e00d84..8cb9e66 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,35 @@ 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): + 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) 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) 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 diff --git a/src/download.py b/src/download.py index 9459417..4c9d586 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 @@ -102,45 +98,18 @@ class Download: 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"): + # Save LaTeX sources to file + with (self.config.p_data_pictures / f"{picture['id']}include.tex").open("w") as file: + file.write(picture["latex"]) - 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 - - 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 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) def download_includes(self): includes = self.api.get("items/include", params={"limit": -1}) diff --git a/templates/html/index.html b/templates/html/index.html index 1ded1eb..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
@@ -135,13 +133,14 @@

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

-

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 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: