From 4216ccce50514ed2a25d73317ec2ccdd7d197fde Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Wed, 14 May 2025 14:53:31 +0200 Subject: [PATCH 01/13] feature: first table implementation --- renderer/fifty_ohm_html_renderer.py | 37 +++++++++++++++++- renderer/table.py | 58 +++++++++++++++++++++++++++++ test/acceptanceTest.md | 2 +- test/test_question.py | 4 +- test/test_table.py | 19 ++++++++++ 5 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 renderer/table.py create mode 100644 test/test_table.py diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py index 9051e3c..cbc1a56 100644 --- a/renderer/fifty_ohm_html_renderer.py +++ b/renderer/fifty_ohm_html_renderer.py @@ -12,6 +12,7 @@ from .picture import Picture from .question import Question from .quote import Quote from .references import References +from .table import Table from .tag import Tag from .underline import Underline from .unit import Unit @@ -38,7 +39,8 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): References, Question, Picture, - Photo + Photo, + Table ) self.question_renderer = question_renderer @@ -195,4 +197,35 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): """ def render_photo(self, token) : - return self.render_photo_helper(token.id, token.ref, token.text, token.number) \ No newline at end of file + return self.render_photo_helper(token.id, token.ref, token.text, token.number) + + @staticmethod + def render_row_helper(row): + return f"""{row}""" + + @staticmethod + def render_cell_helper(cell, alignment, type): + if alignment == "l": + return f"<{type} style='text-align: left;'>{cell}" + elif alignment == "r": + return f"<{type} style='text-align: right;'>{cell}" + elif alignment == "c": + return f"<{type} style='text-align: center;'>{cell}" + else: + return f"<{type}>{cell}" + + def render_table(self, token): + data = token.table + alignment = token.alignment + + table = "" + + for i, row in enumerate(data): + content = "" + for j, cell in enumerate(row[0]): + content += self.render_cell_helper(cell, alignment[j], ("th" if i==0 else "td")) + table += self.render_row_helper(content) + + table += "
" + + return table \ No newline at end of file diff --git a/renderer/table.py b/renderer/table.py new file mode 100644 index 0000000..c95ff06 --- /dev/null +++ b/renderer/table.py @@ -0,0 +1,58 @@ + +from mistletoe.block_token import BlockToken + + +class Table(BlockToken): + @staticmethod + def start(line): + return '|' in line + + @classmethod + def parseHeader(self, line): + line = line.strip().strip('|') # Remove leading/trailing whitespace and pipes + columns = [col.strip() for col in line.split('|')] # Split by pipe | + + alignment = [] + data = [] + + for column in columns: + attr, val = column.split(':') + alignment.append(attr) + data.append(val) + + return data, alignment + + @classmethod + def parseRow(self, line): + line = line.strip().strip('|') + columns = [col.strip() for col in line.split('|')] + + data = [] + + for column in columns: + data.append(column) + + return data + + @classmethod + def read(self, lines): + + table = [] + + header = next(lines) + + data, alignment = self.parseHeader(header) + table.append([data]) + + for line in lines: + if "|" not in line: + break + data = self.parseRow(line) + table.append([data]) + + return [table, alignment] + + def __init__(self, match): + self.table = match[0] + self.alignment = match[1] + print("init") diff --git a/test/acceptanceTest.md b/test/acceptanceTest.md index 5148735..3d5c06c 100644 --- a/test/acceptanceTest.md +++ b/test/acceptanceTest.md @@ -79,7 +79,7 @@ CQ CQ hier ist DL1PZ mit einem allgemeinen Anruf, hier ist DL1PZ und hört. -| c | l | c | l | c | l | +| c: | l: | c: | l: | c: | l: | | | | | | | | | A | [morse:a] | K | [morse:k] | U | [morse:u] | | B | [morse:b] | L | [morse:l] | V | [morse:v] | diff --git a/test/test_question.py b/test/test_question.py index 027c446..0a4d84e 100644 --- a/test/test_question.py +++ b/test/test_question.py @@ -7,7 +7,7 @@ from test.util import paragraph @pytest.mark.html -def test_comment_html(): +def test_question_html(): assertions = { "[question:123]": "123\n", "\n[question:123]\n": "123\n", @@ -24,7 +24,7 @@ def test_comment_html(): @pytest.mark.latex -def test_comment_latex(): +def test_question_latex(): assertions = { "[question:123]": "123", "\n[question:123]\n": "123", diff --git a/test/test_table.py b/test/test_table.py new file mode 100644 index 0000000..6293444 --- /dev/null +++ b/test/test_table.py @@ -0,0 +1,19 @@ +import pytest +from mistletoe import Document + +from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer + + +@pytest.mark.html +def test_question_html(): + assertions = { + "| l: abc | r: abc | X: abc |\n| 123 | 456 | 789 | \n | 987 | 654 | 321 | ": "
abc abc abc
123456789
987654321
\n", + "text\n| l: abc | r: abc | X: abc |\n| 123 | 456 | 789 | \n | 987 | 654 | 321 |\ntext": "text\n
abc abc abc
123456789
987654321
\ntext\n", + } + + def test_function(input): + return f"{input}" + + with FiftyOhmHtmlRenderer(test_function) as renderer: + for assertion in assertions: + assert renderer.render(Document(assertion)) == assertions[assertion] From 74859151147a2804a612e43bf2fe00befc689f8c Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Wed, 14 May 2025 15:55:32 +0200 Subject: [PATCH 02/13] fix: fixed bug in morse code --- renderer/morse.py | 2 +- test/test_morse.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/renderer/morse.py b/renderer/morse.py index ebec4c9..cc0cd02 100644 --- a/renderer/morse.py +++ b/renderer/morse.py @@ -87,4 +87,4 @@ class Morse(SpanToken): return result parse_inner = False - pattern = re.compile(r'"\[morse:\s*(.+)\]"') + pattern = re.compile(r'\[morse:\s*(.+)\]') diff --git a/test/test_morse.py b/test/test_morse.py index e053b68..c568852 100644 --- a/test/test_morse.py +++ b/test/test_morse.py @@ -8,11 +8,11 @@ from test.util import paragraph @pytest.mark.html def test_morse_html(): assertions = { - '"[morse:a]"': FiftyOhmHtmlRenderer.render_morse_helper([[1, 2]]), - '"[morse: a]"': FiftyOhmHtmlRenderer.render_morse_helper([[1, 2]]), - '"[morse: aaa]"': FiftyOhmHtmlRenderer.render_morse_helper([[1, 2], [1, 2], [1, 2]]), - '"[morse: a a]"': FiftyOhmHtmlRenderer.render_morse_helper([[1, 2], [3], [1, 2]]), - '"[morse: db0]"': FiftyOhmHtmlRenderer.render_morse_helper([[2, 1, 1], [2, 1, 1, 1], [2, 2, 2, 2, 2]]), + '[morse:a]': FiftyOhmHtmlRenderer.render_morse_helper([[1, 2]]), + '[morse: a]': FiftyOhmHtmlRenderer.render_morse_helper([[1, 2]]), + '[morse: aaa]': FiftyOhmHtmlRenderer.render_morse_helper([[1, 2], [1, 2], [1, 2]]), + '[morse: a a]': FiftyOhmHtmlRenderer.render_morse_helper([[1, 2], [3], [1, 2]]), + '[morse: db0]': FiftyOhmHtmlRenderer.render_morse_helper([[2, 1, 1], [2, 1, 1, 1], [2, 2, 2, 2, 2]]), } for assertion in assertions: From 9a747dc8bf4b64f64248d131248e6d7b87312932 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Wed, 14 May 2025 15:55:51 +0200 Subject: [PATCH 03/13] feature: html table implemented as envisioned by mistltoe developers --- renderer/fifty_ohm_html_renderer.py | 11 ++---- renderer/table.py | 57 ++++++++++++++++------------- test/test_table.py | 16 ++++---- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py index cbc1a56..e8d03c8 100644 --- a/renderer/fifty_ohm_html_renderer.py +++ b/renderer/fifty_ohm_html_renderer.py @@ -215,17 +215,14 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): return f"<{type}>{cell}" def render_table(self, token): - data = token.table alignment = token.alignment - table = "" - - for i, row in enumerate(data): + + for i, row in enumerate(token.children): content = "" - for j, cell in enumerate(row[0]): - content += self.render_cell_helper(cell, alignment[j], ("th" if i==0 else "td")) + 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 += self.render_row_helper(content) - table += "
" return table \ No newline at end of file diff --git a/renderer/table.py b/renderer/table.py index c95ff06..fae2fb4 100644 --- a/renderer/table.py +++ b/renderer/table.py @@ -1,4 +1,5 @@ +from mistletoe import span_token from mistletoe.block_token import BlockToken @@ -8,20 +9,40 @@ class Table(BlockToken): return '|' in line @classmethod - def parseHeader(self, line): + def parseHeader(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 = [] - data = [] + header = "| " for column in columns: attr, val = column.split(':') alignment.append(attr) - data.append(val) + header += val + " | " - return data, alignment + return header, alignment + @classmethod + def read(cls, lines): + header = next(lines) + header, alignment = cls.parseHeader(header) + rows = [TableRow(header)] + + for line in lines: + if "|" not in line : + break + else : + rows.append(TableRow(line)) + + return alignment, rows + + def __init__(self, match): + self.alignment, self.children = match + + +class TableRow(BlockToken): @classmethod def parseRow(self, line): line = line.strip().strip('|') @@ -30,29 +51,13 @@ class Table(BlockToken): data = [] for column in columns: - data.append(column) + data.append(TableCell(column)) return data - @classmethod - def read(self, lines): - - table = [] - - header = next(lines) - - data, alignment = self.parseHeader(header) - table.append([data]) - - for line in lines: - if "|" not in line: - break - data = self.parseRow(line) - table.append([data]) - - return [table, alignment] - def __init__(self, match): - self.table = match[0] - self.alignment = match[1] - print("init") + self.children = self.parseRow(match) + +class TableCell(BlockToken) : + def __init__(self, match): + super().__init__(match, span_token.tokenize_inner) diff --git a/test/test_table.py b/test/test_table.py index 6293444..46b96d2 100644 --- a/test/test_table.py +++ b/test/test_table.py @@ -5,15 +5,17 @@ from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer @pytest.mark.html -def test_question_html(): +def test_table_html(): assertions = { - "| l: abc | r: abc | X: abc |\n| 123 | 456 | 789 | \n | 987 | 654 | 321 | ": "
abc abc abc
123456789
987654321
\n", - "text\n| l: abc | r: abc | X: abc |\n| 123 | 456 | 789 | \n | 987 | 654 | 321 |\ntext": "text\n
abc abc abc
123456789
987654321
\ntext\n", + "| l: a |" : "
a
\n", + "| r: a |" : "
a
\n", + "| l: a |\n| *b* |" : "
a
b
\n", + "| l: *a* |" : "
a
\n", + "| l: a |\n| [morse:a] |" : "
a
\n\n▄\n\n▄▄▄\n\n
\n", + "| l: a |\n| b |" : "
a
b
\n", + "| l: abc | r: abc | X: abc |\n| 123 | 456 | 789 | \n | 987 | 654 | 321 | ": "
abcabcabc
123456789
987654321
\n", } - def test_function(input): - return f"{input}" - - with FiftyOhmHtmlRenderer(test_function) as renderer: + with FiftyOhmHtmlRenderer() as renderer: for assertion in assertions: assert renderer.render(Document(assertion)) == assertions[assertion] From 8397ded59cd22419911a2d672255082130603621 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Wed, 14 May 2025 18:49:21 +0200 Subject: [PATCH 04/13] feature: render latex table --- renderer/fifty_ohm_latex_renderer.py | 25 +++++++++++++++++++++++-- renderer/table.py | 8 ++++---- test/test_table.py | 14 ++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/renderer/fifty_ohm_latex_renderer.py b/renderer/fifty_ohm_latex_renderer.py index 83891d1..0ce85c6 100644 --- a/renderer/fifty_ohm_latex_renderer.py +++ b/renderer/fifty_ohm_latex_renderer.py @@ -8,6 +8,7 @@ from .photo import Photo from .picture import Picture from .question import Question from .quote import Quote +from .table import Table from .tag import Tag from .underline import Underline @@ -25,7 +26,8 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer): NonbreakingSpacesDots, Question, Picture, - Photo + Photo, + Table ) self.question_renderer = question_renderer @@ -113,4 +115,23 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer): \label{{{ref}}}""" def render_photo(self, token) : - return self.render_picture_helper(token.id, token.ref, token.text, token.number) \ No newline at end of file + return self.render_picture_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}" + + return table diff --git a/renderer/table.py b/renderer/table.py index fae2fb4..a827569 100644 --- a/renderer/table.py +++ b/renderer/table.py @@ -9,7 +9,7 @@ class Table(BlockToken): return '|' in line @classmethod - def parseHeader(cls, line): + 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 | @@ -27,7 +27,7 @@ class Table(BlockToken): @classmethod def read(cls, lines): header = next(lines) - header, alignment = cls.parseHeader(header) + header, alignment = cls.parse_header(header) rows = [TableRow(header)] for line in lines: @@ -44,7 +44,7 @@ class Table(BlockToken): class TableRow(BlockToken): @classmethod - def parseRow(self, line): + def parse_row(cls, line): line = line.strip().strip('|') columns = [col.strip() for col in line.split('|')] @@ -56,7 +56,7 @@ class TableRow(BlockToken): return data def __init__(self, match): - self.children = self.parseRow(match) + self.children = self.parse_row(match) class TableCell(BlockToken) : def __init__(self, match): diff --git a/test/test_table.py b/test/test_table.py index 46b96d2..009905c 100644 --- a/test/test_table.py +++ b/test/test_table.py @@ -2,6 +2,7 @@ import pytest from mistletoe import Document from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer +from renderer.fifty_ohm_latex_renderer import FiftyOhmLaTeXRenderer @pytest.mark.html @@ -19,3 +20,16 @@ def test_table_html(): with FiftyOhmHtmlRenderer() as renderer: for assertion in assertions: assert renderer.render(Document(assertion)) == assertions[assertion] + + +@pytest.mark.latex +def test_thematic_break_latex(): + assertions = { + "| l: a |" : "\\begin{DARCtabular}{l}\na\\\\\n\\end{DARCtabular}", + "| l: a |\n| *b* |" : "\\begin{DARCtabular}{l}\na\\\\\n\\emph{b}\\\\\n\\end{DARCtabular}", + "| l: a | X: b |\n| c | d |" : "\\begin{DARCtabular}{lX}\na & b\\\\\nc & d\\\\\n\\end{DARCtabular}", + } + + with FiftyOhmLaTeXRenderer() as renderer: + for assertion in assertions: + assert renderer.render(Document(assertion)) == assertions[assertion] From c607a8476adb50dd44a4d8825a0b4cbd7433ce97 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Wed, 14 May 2025 21:17:23 +0200 Subject: [PATCH 05/13] refactor: inline row_helper --- renderer/fifty_ohm_html_renderer.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py index e8d03c8..b0d9a5d 100644 --- a/renderer/fifty_ohm_html_renderer.py +++ b/renderer/fifty_ohm_html_renderer.py @@ -199,10 +199,6 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): def render_photo(self, token) : return self.render_photo_helper(token.id, token.ref, token.text, token.number) - @staticmethod - def render_row_helper(row): - return f"""{row}""" - @staticmethod def render_cell_helper(cell, alignment, type): if alignment == "l": @@ -222,7 +218,7 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): 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 += self.render_row_helper(content) + table += f"{content}" table += "" return table \ No newline at end of file From 0b81e8a7cd12547efc9d9c6dbf691f310936aa7d Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Wed, 14 May 2025 23:20:34 +0200 Subject: [PATCH 06/13] fix: include new dependencies in requirements.txt --- requirements.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/requirements.txt b/requirements.txt index fa39cef..2166ff2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,12 @@ +beautifulsoup4==4.13.4 coverage==7.6.9 iniconfig==2.0.0 +jinja2==3.1.6 +markupsafe==3.0.2 mistletoe==1.4.0 packaging==24.2 pluggy==1.5.0 pytest==8.3.4 pytest-cov==6.0.0 +soupsieve==2.7 +typing-extensions==4.13.2 From b135c18c476b0a56adcf927af1ed59814681244c Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Wed, 14 May 2025 23:20:49 +0200 Subject: [PATCH 07/13] refactor: format file with ruff --- test/test_html.py | 64 +++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/test/test_html.py b/test/test_html.py index 72303c9..2d3844c 100644 --- a/test/test_html.py +++ b/test/test_html.py @@ -8,59 +8,56 @@ from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer def parse_katalog(): - with open('data/fragenkatalog3b.json') as fragenkatalog_file: + with open("data/fragenkatalog3b.json") as fragenkatalog_file: fragenkatalog = json.load(fragenkatalog_file) questions = {} - for exampart in fragenkatalog["sections"] : - for chapter in exampart["sections"] : - if "questions" in chapter : - for question in chapter["questions"] : + for exampart in fragenkatalog["sections"]: + for chapter in exampart["sections"]: + if "questions" in chapter: + for question in chapter["questions"]: questions[question["number"]] = question - if "sections" in chapter : - for section in chapter["sections"] : - for question in section["questions"] : + if "sections" in chapter: + for section in chapter["sections"]: + for question in section["questions"]: questions[question["number"]] = question - + return questions -env = Environment(loader=FileSystemLoader('templates/html')) -question_template = env.get_template('question.html') + +env = Environment(loader=FileSystemLoader("templates/html")) +question_template = env.get_template("question.html") questions = parse_katalog() + def question_stub(input): - """ Combines the original question dataset from BNetzA with our internal metadata """ - with open('data/metadata.json') as metadata_file : + """Combines the original question dataset from BNetzA with our internal metadata""" + with open("data/metadata.json") as metadata_file: metadata = json.load(metadata_file) - number = metadata[f"{input}"]["number"] # Fragennummer z.B. AB123 + number = metadata[f"{input}"]["number"] # Fragennummer z.B. AB123 question = questions[number] metadata = metadata[f"{input}"] - if "answer_a" in question : - answers = [ - question["answer_a"], - question["answer_b"], - question["answer_c"], - question["answer_d"] - ] - else : + if "answer_a" in question: + answers = [question["answer_a"], question["answer_b"], question["answer_c"], question["answer_d"]] + else: answers = [] - if metadata["picture_a"] != "" : + if metadata["picture_a"] != "": answer_pictures = [ - metadata["picture_a"], - metadata["picture_b"], - metadata["picture_c"], - metadata["picture_d"] - ] - else : + metadata["picture_a"], + metadata["picture_b"], + metadata["picture_c"], + metadata["picture_d"], + ] + else: answer_pictures = [] - if "picture_question" in question : + if "picture_question" in question: picture_question = metadata["picture_question"] - else : + else: picture_question = "" return question_template.render( @@ -69,9 +66,10 @@ def question_stub(input): layout=metadata["layout"], picture_question=picture_question, answers=answers, - answer_pictures=answer_pictures + answer_pictures=answer_pictures, ) + def test_html(capsys): with capsys.disabled(): with open("test/acceptanceTest.md") as file: @@ -79,5 +77,5 @@ def test_html(capsys): with FiftyOhmHtmlRenderer(question_stub) as renderer: output = renderer.render(Document(content)) with open("test/acceptanceTest.html", "w") as output_file: - pretty = BeautifulSoup(output, 'html.parser').prettify() + pretty = BeautifulSoup(output, "html.parser").prettify() output_file.write(pretty) From 1e7133ca5753732b89e0373311e69f176b9b60be Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Wed, 14 May 2025 23:21:49 +0200 Subject: [PATCH 08/13] feat: improve table tests --- test/test_table.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/test/test_table.py b/test/test_table.py index 009905c..1c507fd 100644 --- a/test/test_table.py +++ b/test/test_table.py @@ -8,13 +8,17 @@ from renderer.fifty_ohm_latex_renderer import FiftyOhmLaTeXRenderer @pytest.mark.html def test_table_html(): assertions = { - "| l: a |" : "
a
\n", - "| r: a |" : "
a
\n", - "| l: a |\n| *b* |" : "
a
b
\n", - "| l: *a* |" : "
a
\n", - "| l: a |\n| [morse:a] |" : "
a
\n\n▄\n\n▄▄▄\n\n
\n", - "| l: a |\n| b |" : "
a
b
\n", - "| l: abc | r: abc | X: abc |\n| 123 | 456 | 789 | \n | 987 | 654 | 321 | ": "
abcabcabc
123456789
987654321
\n", + "| 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', + "| l: a |\n| b |\n| c |": '\n\n\n\n\n\n\n\n\n\n
a
b
c
\n', + "| a |\n| b |\n| c |": "\n\n\n\n\n\n\n\n\n\n
a
b
c
\n", + "| l: a |\n| *b* |": '\n\n\n\n\n\n\n
a
b
\n', + "| l: a |\n| b |\n[table:n_ab:A und B]": '\n\n\n\n\n\n\n
a
b
A und B
\n', + "| 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', + "| 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', } with FiftyOhmHtmlRenderer() as renderer: @@ -25,9 +29,9 @@ def test_table_html(): @pytest.mark.latex def test_thematic_break_latex(): assertions = { - "| l: a |" : "\\begin{DARCtabular}{l}\na\\\\\n\\end{DARCtabular}", - "| l: a |\n| *b* |" : "\\begin{DARCtabular}{l}\na\\\\\n\\emph{b}\\\\\n\\end{DARCtabular}", - "| l: a | X: b |\n| c | d |" : "\\begin{DARCtabular}{lX}\na & b\\\\\nc & d\\\\\n\\end{DARCtabular}", + "| l: a |": "\\begin{DARCtabular}{l}\na\\\\\n\\end{DARCtabular}", + "| l: a |\n| *b* |": "\\begin{DARCtabular}{l}\na\\\\\n\\emph{b}\\\\\n\\end{DARCtabular}", + "| l: a | X: b |\n| c | d |": "\\begin{DARCtabular}{lX}\na & b\\\\\nc & d\\\\\n\\end{DARCtabular}", } with FiftyOhmLaTeXRenderer() as renderer: From 66398285115ffa62244788bfe2b48affddfa60d9 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Wed, 14 May 2025 23:22:27 +0200 Subject: [PATCH 09/13] refactor: simplify rendering of table cells --- renderer/fifty_ohm_html_renderer.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py index b0d9a5d..1c0b912 100644 --- a/renderer/fifty_ohm_html_renderer.py +++ b/renderer/fifty_ohm_html_renderer.py @@ -17,6 +17,8 @@ from .tag import Tag from .underline import Underline from .unit import Unit +table_alignment = {"l": "left", "c": "center", "r": "right"} + class FiftyOhmHtmlRenderer(HtmlRenderer): margin_anchor_id = 0 @@ -199,16 +201,12 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): def render_photo(self, token) : return self.render_photo_helper(token.id, token.ref, token.text, token.number) - @staticmethod + @staticmethod def render_cell_helper(cell, alignment, type): - if alignment == "l": - return f"<{type} style='text-align: left;'>{cell}" - elif alignment == "r": - return f"<{type} style='text-align: right;'>{cell}" - elif alignment == "c": - return f"<{type} style='text-align: center;'>{cell}" - else: - return f"<{type}>{cell}" + 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 From 2fb3a66ce0ca3461c06e59e1497b0af1716a201a Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Wed, 14 May 2025 23:23:05 +0200 Subject: [PATCH 10/13] refactor: format file with ruff --- renderer/fifty_ohm_html_renderer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py index 1c0b912..fc56d42 100644 --- a/renderer/fifty_ohm_html_renderer.py +++ b/renderer/fifty_ohm_html_renderer.py @@ -42,7 +42,7 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): Question, Picture, Photo, - Table + Table, ) self.question_renderer = question_renderer @@ -186,7 +186,7 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): """ - def render_picture(self, token) : + def render_picture(self, token): return self.render_picture_helper(token.id, token.ref, token.text, token.number) @staticmethod @@ -198,7 +198,7 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): """ - def render_photo(self, token) : + def render_photo(self, token): return self.render_photo_helper(token.id, token.ref, token.text, token.number) @staticmethod From 1a8656428d7d772d86e46a3c2d2e87493ca62448 Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Wed, 14 May 2025 23:24:24 +0200 Subject: [PATCH 11/13] fix: parse column alignment with regex --- renderer/table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/table.py b/renderer/table.py index a827569..14c153b 100644 --- a/renderer/table.py +++ b/renderer/table.py @@ -18,7 +18,7 @@ class Table(BlockToken): header = "| " for column in columns: - attr, val = column.split(':') + attr, val = re.match(r"^ ?(?:([lcrX]):)?(.*)", column).group(1, 2) alignment.append(attr) header += val + " | " From 60e8d65e04b3d678959be83fda5d7659bb39277f Mon Sep 17 00:00:00 2001 From: Tim Grelka Date: Wed, 14 May 2025 23:24:33 +0200 Subject: [PATCH 12/13] feat: parse table captions --- renderer/fifty_ohm_html_renderer.py | 13 +++++---- renderer/table.py | 43 +++++++++++++++++++---------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py index fc56d42..22c68e1 100644 --- a/renderer/fifty_ohm_html_renderer.py +++ b/renderer/fifty_ohm_html_renderer.py @@ -210,13 +210,16 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): def render_table(self, token): alignment = token.alignment - table = "" - + 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"{content}" + content += self.render_cell_helper(self.render_inner(cell), alignment[j], ("th" if i == 0 else "td")) + table += f"\n{content}\n" + + if token.caption != "": + table += f"" table += "
{token.caption}
" - return table \ No newline at end of file + return table diff --git a/renderer/table.py b/renderer/table.py index 14c153b..93b1c23 100644 --- a/renderer/table.py +++ b/renderer/table.py @@ -1,3 +1,4 @@ +import re from mistletoe import span_token from mistletoe.block_token import BlockToken @@ -6,13 +7,13 @@ from mistletoe.block_token import BlockToken class Table(BlockToken): @staticmethod def start(line): - return '|' in line + return "|" in line @classmethod - def parse_header(cls, line): + 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 | + line = line.strip().strip("|") # Remove leading/trailing whitespace and pipes + columns = [col.strip() for col in line.split("|")] # Split by pipe | alignment = [] header = "| " @@ -21,32 +22,45 @@ class Table(BlockToken): 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) + name = "" + caption = "" rows = [TableRow(header)] - for line in lines: - if "|" not in line : + # Read table until the end: No more column definitions, caption, or empty line. + line_buffer = [] + next_line = lines.peek() + while next_line is not None and next_line.strip() != "": + maybe_caption = re.match(r"\[table:([^:\]]+):([^:\]]+)\]", next_line) + if maybe_caption is not None: + name, caption = maybe_caption.group(1, 2) + next(lines) break - else : - rows.append(TableRow(line)) + elif "|" not in next_line: + break + else: + line_buffer.append(next(lines)) + next_line = lines.peek() - return alignment, rows + rows.extend([TableRow(line) for line in line_buffer]) + + return alignment, rows, name, caption def __init__(self, match): - self.alignment, self.children = 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('|')] + line = line.strip().strip("|") + columns = [col.strip() for col in line.split("|")] data = [] @@ -58,6 +72,7 @@ class TableRow(BlockToken): def __init__(self, match): self.children = self.parse_row(match) -class TableCell(BlockToken) : + +class TableCell(BlockToken): def __init__(self, match): super().__init__(match, span_token.tokenize_inner) From 5fe7510819e85412ea0dbc80bfa6c23789249343 Mon Sep 17 00:00:00 2001 From: "Prof. Dr. Matthias Jung DL9MJ" Date: Thu, 15 May 2025 09:20:27 +0200 Subject: [PATCH 13/13] feature: caption for tables in LaTeX --- renderer/fifty_ohm_latex_renderer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/renderer/fifty_ohm_latex_renderer.py b/renderer/fifty_ohm_latex_renderer.py index a66d9d5..48ea836 100644 --- a/renderer/fifty_ohm_latex_renderer.py +++ b/renderer/fifty_ohm_latex_renderer.py @@ -133,5 +133,8 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer): table += r"\\"+ "\n" table += "\\end{DARCtabular}" + if token.caption != "" : + table += f"\\captionof{{figure}}{{{token.caption}}}\n" + table += f"\\label{{{token.name}}}\n" return table