Merge branch 'main' into feature-alt-texts
This commit is contained in:
@@ -3,6 +3,7 @@ from mistletoe import Document, HtmlRenderer
|
||||
|
||||
from .comment import BlockComment
|
||||
from .dash import Dash
|
||||
from .formula import Formula
|
||||
from .halfwidth_spaces import HalfwidthSpaces
|
||||
from .include import Include
|
||||
from .morse import Morse
|
||||
@@ -13,7 +14,7 @@ from .qso import Qso
|
||||
from .question import Question
|
||||
from .quote import Quote
|
||||
from .references import References
|
||||
from .table import Table
|
||||
from .table import Table, TableBody, TableCell, TableHeader, TableRow
|
||||
from .tag import Tag
|
||||
from .underline import Underline
|
||||
from .unit import Unit
|
||||
@@ -28,7 +29,17 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
||||
ref_id = 0
|
||||
|
||||
def __init__(
|
||||
self, *extras, question_renderer=None, picture_handler=None, photo_handler=None, include_handler=None, **kwargs
|
||||
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,
|
||||
@@ -46,8 +57,13 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
||||
Picture,
|
||||
Photo,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Qso,
|
||||
Include,
|
||||
Formula,
|
||||
*extras,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -57,6 +73,21 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
||||
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
|
||||
if section_url is not None:
|
||||
self.section_url = section_url
|
||||
|
||||
# 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 " – "
|
||||
|
||||
@@ -104,7 +135,7 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
||||
for char in morse_code:
|
||||
result += '<span class="morse_char">\n'
|
||||
for symbol in char:
|
||||
result += '<span class="morse_char">\n'
|
||||
result += '<span class="morse_symbol">\n'
|
||||
if symbol == 1:
|
||||
result += "▄"
|
||||
elif symbol == 2:
|
||||
@@ -171,10 +202,47 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
||||
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, Photo, or Table token
|
||||
if isinstance(child, Picture | Photo | Table):
|
||||
# Get the reference ID (ref for Picture/Photo, name for Table)
|
||||
ref_id = getattr(child, "ref", None) or getattr(child, "name", None)
|
||||
|
||||
if ref_id: # Only number figures with a reference ID
|
||||
self.figure_counter += 1
|
||||
hierarchical_num = self._format_figure_number(self.figure_counter)
|
||||
self.figure_map[ref_id] = 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'<a href="{self.section_url}#ref_{token.first}"'
|
||||
f" onclick=\"highlightRef('{token.first}');\">{self.ref_id}</a>"
|
||||
f" onclick=\"highlightRef('{token.first}');\">{figure_num}</a>"
|
||||
)
|
||||
|
||||
def render_question(self, token):
|
||||
@@ -220,28 +288,47 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
||||
alt_text = self.photo_handler(token.id) or alt_text
|
||||
return self.render_photo_helper(token.id, token.ref, token.text, token.number, alt_text)
|
||||
|
||||
@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: 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}"'
|
||||
|
||||
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"
|
||||
table = f'<table class="table table-hover"{table_attrs}>\n{self.render_inner(token)}'
|
||||
|
||||
if token.caption != "":
|
||||
table += f"<caption>{token.caption}</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"<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"
|
||||
|
||||
@@ -10,6 +10,10 @@ class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Add "S" suffix to edition for slides
|
||||
if self.edition:
|
||||
self.edition = f"{self.edition}S"
|
||||
|
||||
def render_slide_break(self, token):
|
||||
inner = self.render_inner(token)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from .photo import Photo
|
||||
from .picture import Picture
|
||||
from .question import Question
|
||||
from .quote import Quote
|
||||
from .table import Table
|
||||
from .table import Table, TableBody, TableCell, TableHeader, TableRow
|
||||
from .tag import Tag
|
||||
from .underline import Underline
|
||||
|
||||
@@ -28,9 +28,17 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer):
|
||||
Picture,
|
||||
Photo,
|
||||
Table,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
)
|
||||
self.question_renderer = question_renderer
|
||||
|
||||
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_document(self, token):
|
||||
self.footnotes.update(token.footnotes)
|
||||
return self.render_inner(token)
|
||||
@@ -118,23 +126,25 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer):
|
||||
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}"
|
||||
table = "\\begin{DARCtabular}" + f"{self.render_inner(token)}" + "\\end{DARCtabular}"
|
||||
if token.caption != "":
|
||||
table += f"\\captionof{{figure}}{{{token.caption}}}\n"
|
||||
table += f"\\label{{{token.name}}}\n"
|
||||
|
||||
return table
|
||||
|
||||
def render_table_body(self, token: TableBody):
|
||||
return self.render_inner(token)
|
||||
|
||||
def render_table_row(self, token: TableRow):
|
||||
return rf"{self.render_inner(token, ' & ')}\\" + "\n"
|
||||
|
||||
def render_table_header(self, token: TableHeader):
|
||||
align = ""
|
||||
for alignment in token.alignment:
|
||||
align += alignment
|
||||
|
||||
return f"{{{align}}}\n{self.render_table_row(token)}"
|
||||
|
||||
def render_table_cell(self, token: TableCell):
|
||||
return self.render_inner(token)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import re
|
||||
|
||||
from mistletoe.block_token import BlockToken
|
||||
|
||||
|
||||
class Formula(BlockToken):
|
||||
r"""
|
||||
Block token ("$ a \cdot b $").
|
||||
Transfroms a line that contains only a formula enclosed in dollar signs into a Formula token.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def start(line):
|
||||
return re.match(r"^\s*\$[^$]+\$\s*$", line)
|
||||
|
||||
@classmethod
|
||||
def check_interrupts_paragraph(cls, lines):
|
||||
return cls.start(lines.peek())
|
||||
|
||||
@classmethod
|
||||
def read(cls, lines):
|
||||
line = next(lines) # Consume the line with the formula
|
||||
formula = re.match(r"^\s*\$([^$]+)\$\s*$", line).group(1)
|
||||
return formula
|
||||
|
||||
def __init__(self, match):
|
||||
self.formula = match
|
||||
+81
-41
@@ -1,37 +1,99 @@
|
||||
import re
|
||||
from enum import Enum
|
||||
|
||||
from mistletoe import span_token
|
||||
from mistletoe.block_token import BlockToken
|
||||
|
||||
|
||||
class Table(BlockToken):
|
||||
class CellAlignment(Enum):
|
||||
LEFT = "l"
|
||||
CENTER = "c"
|
||||
RIGHT = "r"
|
||||
EXPAND = "X"
|
||||
|
||||
|
||||
class TableCell(BlockToken):
|
||||
alignment: CellAlignment
|
||||
header: bool
|
||||
|
||||
@staticmethod
|
||||
def start(line):
|
||||
return "|" in line
|
||||
def start(_):
|
||||
return False
|
||||
|
||||
def __init__(self, match, alignment: CellAlignment, header=False):
|
||||
super().__init__(match, span_token.tokenize_inner)
|
||||
self.alignment, self.header = alignment, header
|
||||
|
||||
|
||||
class TableRow(BlockToken):
|
||||
@staticmethod
|
||||
def start(_):
|
||||
return False
|
||||
|
||||
@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 |
|
||||
def parse(cls, line) -> list[str]:
|
||||
line = line.strip().strip("|")
|
||||
return [col.strip() for col in line.split("|")]
|
||||
|
||||
alignment = []
|
||||
header = "| "
|
||||
def __init__(self, match, alignment: list[CellAlignment]):
|
||||
self.children = [TableCell(cell, alignment[i]) for i, cell in enumerate(self.parse(match))]
|
||||
|
||||
|
||||
class TableHeader(TableRow):
|
||||
alignment: list[CellAlignment]
|
||||
|
||||
@staticmethod
|
||||
def start(_):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def parse(cls, line) -> list[str]:
|
||||
alignment: list[CellAlignment] = []
|
||||
columns: list[str] = []
|
||||
for column in super().parse(line):
|
||||
attr: CellAlignment
|
||||
value: str
|
||||
attr, value = re.match(r"^ ?(?:([lcrX]):)? ?(.*)", column).group(1, 2)
|
||||
|
||||
for column in columns:
|
||||
attr, val = re.match(r"^ ?(?:([lcrX]):)?(.*)", column).group(1, 2)
|
||||
alignment.append(attr)
|
||||
header += val + " | "
|
||||
columns.append(value)
|
||||
|
||||
return header, alignment
|
||||
return columns, alignment
|
||||
|
||||
def __init__(self, match):
|
||||
columns, self.alignment = self.parse(match)
|
||||
self.children = [TableCell(cell, self.alignment[i], True) for i, cell in enumerate(columns)]
|
||||
|
||||
|
||||
class TableBody(BlockToken):
|
||||
header: bool
|
||||
|
||||
@staticmethod
|
||||
def start(_):
|
||||
return False
|
||||
|
||||
def __init__(self, rows: TableRow, header=False):
|
||||
self.children, self.header = rows, header
|
||||
|
||||
|
||||
class Table(BlockToken):
|
||||
name: str
|
||||
caption: str
|
||||
header: list[TableHeader]
|
||||
rows: list[TableRow]
|
||||
|
||||
@staticmethod
|
||||
def start(line):
|
||||
return line.lstrip().startswith("|")
|
||||
|
||||
@classmethod
|
||||
def read(cls, lines):
|
||||
header = next(lines)
|
||||
header, alignment = cls.parse_header(header)
|
||||
header = TableHeader(next(lines))
|
||||
alignment = header.alignment
|
||||
|
||||
name = ""
|
||||
caption = ""
|
||||
rows = [TableRow(header)]
|
||||
children = [TableBody([header], True)]
|
||||
|
||||
# Read table until the end: No more column definitions, caption, or empty line.
|
||||
line_buffer = []
|
||||
@@ -48,31 +110,9 @@ class Table(BlockToken):
|
||||
line_buffer.append(next(lines))
|
||||
next_line = lines.peek()
|
||||
|
||||
rows.extend([TableRow(line) for line in line_buffer])
|
||||
children.append(TableBody([TableRow(line, alignment) for line in line_buffer]))
|
||||
|
||||
return alignment, rows, name, caption
|
||||
return children, 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)
|
||||
self.children, self.name, self.caption = match
|
||||
|
||||
Reference in New Issue
Block a user