diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py index 9051e3c..22c68e1 100644 --- a/renderer/fifty_ohm_html_renderer.py +++ b/renderer/fifty_ohm_html_renderer.py @@ -12,10 +12,13 @@ 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 +table_alignment = {"l": "left", "c": "center", "r": "right"} + class FiftyOhmHtmlRenderer(HtmlRenderer): margin_anchor_id = 0 @@ -38,7 +41,8 @@ class FiftyOhmHtmlRenderer(HtmlRenderer): References, Question, Picture, - Photo + Photo, + Table, ) self.question_renderer = question_renderer @@ -182,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 @@ -194,5 +198,28 @@ 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 + def render_photo(self, token): + 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" + + if token.caption != "": + table += f"" + table += "
{token.caption}
" + + return table diff --git a/renderer/fifty_ohm_latex_renderer.py b/renderer/fifty_ohm_latex_renderer.py index fe242c4..48ea836 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,26 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer): \label{{{ref}}}""" 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) + + 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}" + if token.caption != "" : + table += f"\\captionof{{figure}}{{{token.caption}}}\n" + table += f"\\label{{{token.name}}}\n" + + return table 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/renderer/table.py b/renderer/table.py new file mode 100644 index 0000000..93b1c23 --- /dev/null +++ b/renderer/table.py @@ -0,0 +1,78 @@ +import re + +from mistletoe import span_token +from mistletoe.block_token import BlockToken + + +class Table(BlockToken): + @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) + name = "" + caption = "" + rows = [TableRow(header)] + + # 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 + elif "|" not in next_line: + break + else: + line_buffer.append(next(lines)) + next_line = lines.peek() + + rows.extend([TableRow(line) for line in line_buffer]) + + return alignment, rows, 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) 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 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_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) 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: diff --git a/test/test_table.py b/test/test_table.py new file mode 100644 index 0000000..1c507fd --- /dev/null +++ b/test/test_table.py @@ -0,0 +1,39 @@ +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 +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', + "| 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: + 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]