Pull request #30: Feature/table
Merge in FRAG/50ohm.de from feature/table to main * commit '5fe7510819e85412ea0dbc80bfa6c23789249343': feature: caption for tables in LaTeX feat: parse table captions fix: parse column alignment with regex refactor: format file with ruff refactor: simplify rendering of table cells feat: improve table tests refactor: format file with ruff fix: include new dependencies in requirements.txt refactor: inline row_helper feature: render latex table feature: html table implemented fix: fixed bug in morse code feature: first table implementation
This commit is contained in:
@@ -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):
|
||||
</figure>
|
||||
"""
|
||||
|
||||
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):
|
||||
</figure>
|
||||
"""
|
||||
|
||||
def render_photo(self, token) :
|
||||
return self.render_photo_helper(token.id, token.ref, token.text, token.number)
|
||||
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 = "<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"<tr>\n{content}</tr>\n"
|
||||
|
||||
if token.caption != "":
|
||||
table += f"<caption>{token.caption}</caption>"
|
||||
table += "</table>"
|
||||
|
||||
return table
|
||||
|
||||
@@ -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)
|
||||
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
|
||||
|
||||
+1
-1
@@ -87,4 +87,4 @@ class Morse(SpanToken):
|
||||
return result
|
||||
|
||||
parse_inner = False
|
||||
pattern = re.compile(r'"\[morse:\s*(.+)\]"')
|
||||
pattern = re.compile(r'\[morse:\s*(.+)\]')
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -79,7 +79,7 @@ CQ CQ hier ist DL1PZ mit einem allgemeinen Anruf, hier ist DL1PZ und hört.
|
||||
</indepth>
|
||||
|
||||
<webmargin>
|
||||
| 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] |
|
||||
|
||||
+31
-33
@@ -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)
|
||||
|
||||
+5
-5
@@ -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:
|
||||
|
||||
@@ -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 |": '<table>\n<tr>\n<th style="text-align: left;">a</th>\n</tr>\n</table>\n',
|
||||
"| r: a |": '<table>\n<tr>\n<th style="text-align: right;">a</th>\n</tr>\n</table>\n',
|
||||
"| c: a |": '<table>\n<tr>\n<th style="text-align: center;">a</th>\n</tr>\n</table>\n',
|
||||
"| l: a |\n| b |": '<table>\n<tr>\n<th style="text-align: left;">a</th>\n</tr>\n<tr>\n<td style="text-align: left;">b</td>\n</tr>\n</table>\n',
|
||||
"| l: a |\n| b |\n| c |": '<table>\n<tr>\n<th style="text-align: left;">a</th>\n</tr>\n<tr>\n<td style="text-align: left;">b</td>\n</tr>\n<tr>\n<td style="text-align: left;">c</td>\n</tr>\n</table>\n',
|
||||
"| a |\n| b |\n| c |": "<table>\n<tr>\n<th>a</th>\n</tr>\n<tr>\n<td>b</td>\n</tr>\n<tr>\n<td>c</td>\n</tr>\n</table>\n",
|
||||
"| l: a |\n| *b* |": '<table>\n<tr>\n<th style="text-align: left;">a</th>\n</tr>\n<tr>\n<td style="text-align: left;"><em>b</em></td>\n</tr>\n</table>\n',
|
||||
"| l: a |\n| b |\n[table:n_ab:A und B]": '<table>\n<tr>\n<th style="text-align: left;">a</th>\n</tr>\n<tr>\n<td style="text-align: left;">b</td>\n</tr>\n<caption>A und B</caption></table>\n',
|
||||
"| l: *a* |": '<table>\n<tr>\n<th style="text-align: left;"><em>a</em></th>\n</tr>\n</table>\n',
|
||||
"| l: a |\n| [morse:a] |": '<table>\n<tr>\n<th style="text-align: left;">a</th>\n</tr>\n<tr>\n<td style="text-align: left;"><span class="morse"><span class="morse_char">\n<span class="morse_char">\n▄</span>\n<span class="morse_char">\n▄▄▄</span>\n</span>\n</span></td>\n</tr>\n</table>\n',
|
||||
"| l: abc | r: abc | X: abc |\n| 123 | 456 | 789 | \n | 987 | 654 | 321 | ": '<table>\n<tr>\n<th style="text-align: left;">abc</th>\n<th style="text-align: right;">abc</th>\n<th>abc</th>\n</tr>\n<tr>\n<td style="text-align: left;">123</td>\n<td style="text-align: right;">456</td>\n<td>789</td>\n</tr>\n<tr>\n<td style="text-align: left;">987</td>\n<td style="text-align: right;">654</td>\n<td>321</td>\n</tr>\n</table>\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]
|
||||
Reference in New Issue
Block a user