from jinja2 import Environment, FileSystemLoader
from mistletoe import Document, HtmlRenderer
from .comment import BlockComment
from .dash import Dash
from .halfwidth_spaces import HalfwidthSpaces
from .include import Include
from .morse import Morse
from .nonbreaking_spaces import NonbreakingSpaces, NonbreakingSpacesDots
from .photo import Photo
from .picture import Picture
from .qso import Qso
from .question import Question
from .quote import Quote
from .references import References
from .table import Table, TableBody, TableCell, TableHeader, TableRow
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
margin_id = 0
section_url = "section.html"
ref_id = 0
def __init__(
self,
*extras,
question_renderer=None,
picture_handler=None,
photo_handler=None,
include_handler=None,
edition=None,
chapter=None,
section=None,
**kwargs,
):
super().__init__(
Dash,
BlockComment,
Quote,
Unit,
Underline,
Morse,
Tag,
HalfwidthSpaces,
NonbreakingSpaces,
NonbreakingSpacesDots,
References,
Question,
Picture,
Photo,
Table,
TableHeader,
TableRow,
TableCell,
TableBody,
Qso,
Include,
*extras,
**kwargs,
)
self.question_renderer = question_renderer
self.picture_handler = picture_handler
self.photo_handler = photo_handler
self.include_handler = include_handler
# Figure numbering context
self.edition = edition
self.chapter = chapter
self.section = section
# Figure map: stores ref -> hierarchical number mapping
self.figure_map = {}
# Single unified counter for all figure types (pictures, photos, tables)
self.figure_counter = 0
def render_dash(self, token):
return " – "
def render_block_comment(self, token):
return None
def render_quote(self, token):
return f"„{self.render_inner(token)}“"
def render_underline(self, token):
return f"{self.render_inner(token)}"
@classmethod
def render_unit(cls, token: Unit):
unit = token.prefix + cls.convert_unit_helper(token.unit)
if token.unit in ["°", "%"]:
# Special cases with no space between value and unit.
return f"{token.value}{unit}"
else:
# Default case is rendered with a narrow no-break space.
return f"{token.value} {unit}"
units = {
"Ohm": "Ω",
}
@classmethod
def convert_unit_helper(cls, unit: str) -> str:
"""Converts human-typable units to their preferred representation.
:param str unit: The unit to convert
"""
if unit in cls.units.keys():
return cls.units[unit]
else:
return unit
def render_thematic_break(self, token):
self.margin_anchor_id += 1
return f''
@staticmethod
def render_morse_helper(morse_code):
result = ''
for char in morse_code:
result += '\n'
for symbol in char:
result += '\n'
if symbol == 1:
result += "▄"
elif symbol == 2:
result += "▄▄▄"
elif symbol == 3:
result += " "
result += "\n"
result += "\n"
result += ""
return result
def render_morse(self, token):
morse_code = Morse.convert_to_morse_code(token.content)
return self.render_morse_helper(morse_code)
@staticmethod
def render_tag_helper(type, content, margin_id, margin_anchor_id):
"""This function is used to render the different types of tags. It is
used in the HtmlRenderer class and also in the test class"""
env = Environment(loader=FileSystemLoader("templates/html"))
margin_template = env.get_template("margin.html")
return margin_template.render(
type=type,
content=content,
id=margin_id,
margin_anchor_id=margin_anchor_id,
)
def render_tag(self, token):
if token.tagtype == "latexonly":
return ""
elif token.tagtype == "webonly":
return self.render_inner(token)
self.margin_id += 1
if token.tagtype == "webmargin":
type = "margin"
elif token.tagtype == "webtip":
type = "tip"
elif token.tagtype == "webindepth":
type = "indepth"
else:
type = token.tagtype
return self.render_tag_helper(type, self.render_inner(token), self.margin_id, self.margin_anchor_id)
def render_qso(self, token):
self.margin_id += 1
qso = ""
for child in token.children:
direction = "other" if child.received else "own"
qso += f'
{self.render_inner(child)}
\n'
return self.render_tag_helper("qso", qso, self.margin_id, self.margin_anchor_id)
def render_halfwidth_spaces(self, token):
return f"{token.first}. {token.second}."
def render_nonbreaking_spaces(self, token):
return f"{token.first} {token.second}"
def render_nonbreaking_spaces_dots(self, token):
lookup = {"": "", " ": " "}
return f"{lookup[token.first]}{token.second}{lookup[token.third]}"
def collect_figures(self, document):
"""First pass: collect all figures and assign hierarchical numbers."""
self.figure_counter = 0
self.figure_map = {}
self._collect_figures_recursive(document)
def _collect_figures_recursive(self, token):
"""Recursively traverse the document tree to collect figures."""
if hasattr(token, "children") and token.children is not None:
for child in token.children:
# Check if this is a Picture token
if isinstance(child, Picture):
self.figure_counter += 1
hierarchical_num = self._format_figure_number(self.figure_counter)
self.figure_map[child.ref] = hierarchical_num
child.number = hierarchical_num
# Check if this is a Photo token
elif isinstance(child, Photo):
self.figure_counter += 1
hierarchical_num = self._format_figure_number(self.figure_counter)
self.figure_map[child.ref] = hierarchical_num
child.number = hierarchical_num
# Check if this is a Table token
elif isinstance(child, Table):
if child.name: # Only number tables with names
self.figure_counter += 1
hierarchical_num = self._format_figure_number(self.figure_counter)
self.figure_map[child.name] = hierarchical_num
child.number = hierarchical_num
else:
child.number = ""
# Recurse into children
self._collect_figures_recursive(child)
def _format_figure_number(self, counter):
"""Format the hierarchical figure number based on context."""
if self.edition and self.chapter and self.section:
return f"{self.edition}-{self.chapter}.{self.section}.{counter}"
else:
# Fallback for tests or when context is not provided
return str(counter)
def render_references(self, token):
# Look up the figure number from the map
figure_num = self.figure_map.get(token.first, "?")
return (
f'{figure_num}"
)
def render_question(self, token):
return self.question_renderer(token.question_number)
def render_document(self, token: Document) -> str:
self.footnotes.update(token.footnotes)
inner = self.render_inner(token, "\n")
return f"{inner}\n" if inner else ""
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]))
@staticmethod
def render_picture_helper(id, ref, text, number):
return f"""
Abbildung {number}: {text}
"""
def render_picture(self, token):
if self.picture_handler is not None:
self.picture_handler(token.id)
return self.render_picture_helper(token.id, token.ref, token.text, token.number)
@staticmethod
def render_photo_helper(id, ref, text, number):
return f"""
Abbildung {number}: {text}
"""
def render_photo(self, token):
if self.photo_handler is not None:
self.photo_handler(token.id)
return self.render_photo_helper(token.id, token.ref, token.text, token.number)
def render_table(self, token: Table):
# Add id and name attributes if table has a name
table_attrs = ""
if token.name:
table_attrs = f' id="ref_{token.name}" name="{token.name}"'
table = f'
\n{self.render_inner(token)}'
if token.caption != "":
# Include hierarchical number in caption if available
caption_text = token.caption
if hasattr(token, "number") and token.number:
caption_text = f"Tabelle {token.number}: {token.caption}"
table += f"