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}{type}>\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"{token.caption}"
+ table += "
"
+
+ 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',
+ "| r: a |": '\n',
+ "| c: a |": '\n',
+ "| l: a |\n| b |": '\n',
+ "| l: a |\n| b |\n| c |": '\n',
+ "| a |\n| b |\n| c |": "\n",
+ "| l: a |\n| *b* |": '\n',
+ "| l: a |\n| b |\n[table:n_ab:A und B]": '\n',
+ "| l: *a* |": '\n',
+ "| l: a |\n| [morse:a] |": '\n\n| a | \n
\n\n| \n\n▄\n\n▄▄▄\n\n | \n
\n
\n',
+ "| l: abc | r: abc | X: abc |\n| 123 | 456 | 789 | \n | 987 | 654 | 321 | ": '\n\n| abc | \nabc | \nabc | \n
\n\n| 123 | \n456 | \n789 | \n
\n\n| 987 | \n654 | \n321 | \n
\n
\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]