- Editions replaced by INSTEG, HAREC and UPPGRADERING (Instegscertifikat -> HAREC) - Question pool optional; simulator, homework tool and solutions switch off without one - All UI strings, templates, renderers and project JS localised to sv-SE (decimal comma, Swedish non-breaking-space rules, Å/Ä/Ö in Morse table) - DARC branding, membership CTA and German links removed; attribution footer added - Page names centralised in src/pages.py - README and docs rewritten in Swedish Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
334 lines
11 KiB
Python
334 lines
11 KiB
Python
import re
|
|
|
|
from jinja2 import Environment, FileSystemLoader
|
|
from mistletoe import HtmlRenderer
|
|
|
|
from .comment import BlockComment
|
|
from .dash import Dash
|
|
from .document import Document
|
|
from .formula import Formula
|
|
from .halfwidth_spaces import HalfwidthSpaces
|
|
from .image import Image
|
|
from .include import Include
|
|
from .index import Index
|
|
from .morse import Morse
|
|
from .nonbreaking_spaces import NonbreakingSpaces, NonbreakingSpacesDots
|
|
from .qso import Qso
|
|
from .question import Question
|
|
from .quote import Quote
|
|
from .reference import Reference
|
|
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
|
|
|
|
def __init__(
|
|
self,
|
|
*extras,
|
|
question_renderer=None,
|
|
picture_handler=None,
|
|
photo_handler=None,
|
|
include_handler=None,
|
|
edition=None,
|
|
chapter=None,
|
|
section=None,
|
|
section_url=None,
|
|
**kwargs,
|
|
):
|
|
super().__init__(
|
|
Dash,
|
|
BlockComment,
|
|
Quote,
|
|
Unit,
|
|
Underline,
|
|
Morse,
|
|
Tag,
|
|
HalfwidthSpaces,
|
|
NonbreakingSpaces,
|
|
NonbreakingSpacesDots,
|
|
Reference,
|
|
Question,
|
|
Image,
|
|
Table,
|
|
TableHeader,
|
|
TableRow,
|
|
TableCell,
|
|
TableBody,
|
|
Qso,
|
|
Include,
|
|
Formula,
|
|
Index,
|
|
*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
|
|
|
|
# Set section URL if provided, otherwise use default
|
|
self.section_url = section_url if section_url is not None else "section.html"
|
|
|
|
self.references: dict[str, str] = {}
|
|
|
|
# Keep track of index ids already emitted in this document.
|
|
self.index_anchor_ids = set()
|
|
|
|
def _format_reference_label(self, marker: str):
|
|
if marker in self.references:
|
|
label = self.references[marker]
|
|
if self.edition and self.chapter and self.section:
|
|
label = f"{self.edition}-{self.chapter}.{self.section}.{label}"
|
|
return label
|
|
else:
|
|
return None
|
|
|
|
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"<u>{self.render_inner(token)}</u>"
|
|
|
|
@classmethod
|
|
def render_unit(cls, token: Unit):
|
|
unit = token.prefix + cls.convert_unit_helper(token.unit)
|
|
if token.unit == "°":
|
|
# Special case 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": "Ω",
|
|
"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'<a id="margin_{self.margin_anchor_id}"></a>'
|
|
|
|
@staticmethod
|
|
def render_morse_helper(morse_code):
|
|
result = '<span class="morse">'
|
|
for char in morse_code:
|
|
result += '<span class="morse_char">\n'
|
|
for symbol in char:
|
|
result += '<span class="morse_symbol">\n'
|
|
if symbol == 1:
|
|
result += "▄"
|
|
elif symbol == 2:
|
|
result += "▄▄▄"
|
|
elif symbol == 3:
|
|
result += " "
|
|
result += "</span>\n"
|
|
result += "</span>\n"
|
|
result += "</span>"
|
|
|
|
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'<div class="qso_{direction}">{self.render_inner(child)}</div>\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 render_reference(self, token: Reference):
|
|
# Look up the figure number from the map
|
|
label = self._format_reference_label(token.marker) or "?"
|
|
return (
|
|
f'<a href="{self.section_url}#ref_{token.marker}" onclick="highlightRef(\'{token.marker}\');">{label}</a>'
|
|
)
|
|
|
|
def render_question(self, token):
|
|
return self.question_renderer(token.question_number)
|
|
|
|
def render_document(self, token: Document) -> str:
|
|
self.references.update(token.references)
|
|
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]))
|
|
|
|
def render_picture_helper(self, id, marker, text, label, alt_text):
|
|
return (
|
|
f'<figure class="picture" id="ref_{marker}" name="{marker}">\n'
|
|
f' <img src="pictures/{id}.svg" alt="{alt_text}">\n'
|
|
f" <figcaption>Figur {label}: {text}</figcaption>\n"
|
|
"</figure>\n"
|
|
)
|
|
|
|
@staticmethod
|
|
def render_photo_helper(id, ref, text, label, alt_text):
|
|
return f"""
|
|
<figure class="photo" id="ref_{ref}" name="{ref}">
|
|
<img src="photos/{id}.png" alt="{alt_text}">
|
|
<figcaption>Figur {label}: {text}</figcaption>
|
|
</figure>
|
|
"""
|
|
|
|
def render_image(self, token: Image):
|
|
label = self._format_reference_label(token.marker) or "?"
|
|
|
|
# Only supported kinds are photo and picture
|
|
if token.kind == "photo":
|
|
alt_text = ""
|
|
if self.photo_handler is not None:
|
|
alt_text = self.photo_handler(token.id) or alt_text
|
|
return self.render_photo_helper(token.id, token.marker, token.text, label, alt_text)
|
|
if token.kind == "picture":
|
|
alt_text = ""
|
|
if self.picture_handler is not None:
|
|
alt_text = self.picture_handler(token.id)
|
|
return self.render_picture_helper(token.id, token.marker, token.text, label, alt_text)
|
|
return ""
|
|
|
|
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'<table class="table table-hover"{table_attrs}>\n{self.render_inner(token)}'
|
|
|
|
if token.caption != "":
|
|
# Include hierarchical number in caption if the table is named.
|
|
caption_text = token.caption
|
|
label = self._format_reference_label(token.name)
|
|
if label:
|
|
caption_text = f"Tabell {label}: {token.caption}"
|
|
table += f"<caption>{caption_text}</caption>\n"
|
|
|
|
table += "</table>"
|
|
|
|
return table
|
|
|
|
def render_table_row(self, token: TableRow):
|
|
return f"<tr>\n{self.render_inner(token)}</tr>\n"
|
|
|
|
def render_table_header(self, token: TableHeader):
|
|
return self.render_table_row(token)
|
|
|
|
def render_table_cell(self, token: TableCell):
|
|
type = "th" if token.header else "td"
|
|
style = ""
|
|
if token.alignment in table_alignment:
|
|
style = f' style="text-align: {table_alignment[token.alignment]};"'
|
|
return f"<{type}{style}>{self.render_inner(token)}</{type}>\n"
|
|
|
|
def render_table_body(self, token: TableBody):
|
|
if token.children is None or len(token.children) == 0:
|
|
return None
|
|
else:
|
|
type = "thead" if token.header else "tbody"
|
|
return f"<{type}>\n{self.render_inner(token)}</{type}>\n"
|
|
|
|
def render_include(self, token):
|
|
return self.include_handler(token.ident)
|
|
|
|
def render_formula(self, token):
|
|
return f"\n$${token.formula}$$\n"
|
|
|
|
@staticmethod
|
|
def _normalize_index_part(value: str) -> str:
|
|
normalized = value.strip().lower()
|
|
normalized = re.sub(r"\s+", "_", normalized)
|
|
normalized = re.sub(r"[^\w]", "_", normalized)
|
|
normalized = re.sub(r"_+", "_", normalized).strip("_")
|
|
return normalized or "index"
|
|
|
|
def render_index(self, token):
|
|
first = self._normalize_index_part(token.first)
|
|
if token.second:
|
|
second = self._normalize_index_part(token.second)
|
|
span_id = f"index_{first}__{second}"
|
|
else:
|
|
span_id = f"index_{first}"
|
|
|
|
if span_id in self.index_anchor_ids:
|
|
return ""
|
|
|
|
self.index_anchor_ids.add(span_id)
|
|
return f'<span id="{span_id}" class="index-anchor" aria-hidden="true"></span>'
|