diff --git a/renderer/document.py b/renderer/document.py
index 71cad24..615a799 100644
--- a/renderer/document.py
+++ b/renderer/document.py
@@ -2,6 +2,9 @@ import mistletoe
class Document(mistletoe.Document):
- # margin_count = 0
- # margin_anchor_count = 0
- references = {}
+ def __init__(self, *args, **kwargs):
+ # references must exist before super().__init__ tokenizes the lines,
+ # since ReferencedToken reads it from _root_node during tokenization.
+ self.references: dict[str, str] = {}
+
+ super().__init__(*args, **kwargs)
diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py
index 5b11c6f..9e4fad2 100644
--- a/renderer/fifty_ohm_html_renderer.py
+++ b/renderer/fifty_ohm_html_renderer.py
@@ -8,16 +8,15 @@ 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 .photo import Photo
-from .picture import Picture
from .qso import Qso
from .question import Question
from .quote import Quote
-from .references import References
+from .reference import Reference
from .table import Table, TableBody, TableCell, TableHeader, TableRow
from .tag import Tag
from .underline import Underline
@@ -30,14 +29,6 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
margin_anchor_id = 0
margin_id = 0
- edition: str = None
- chapter: str = None
- section: str = None
- section_url: str = None
-
- # TODO: Add type
- references = {}
-
def __init__(
self,
*extras,
@@ -62,10 +53,9 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
HalfwidthSpaces,
NonbreakingSpaces,
NonbreakingSpacesDots,
- References,
+ Reference,
Question,
- Picture,
- Photo,
+ Image,
Table,
TableHeader,
TableRow,
@@ -90,23 +80,21 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
self.section = section
# Set section URL if provided, otherwise use default
- if section_url is not None:
- self.section_url = section_url
+ self.section_url = section_url if section_url is not None else "section.html"
- # Figure map: stores ref -> hierarchical number mapping
- self.figure_map = {}
-
- # Single unified counter for all figure types (pictures, photos, tables)
- self.figure_counter = 0
+ self.references: dict[str, str] = {}
# Keep track of index ids already emitted in this document.
self.index_anchor_ids = set()
- def get_reference_label(self, marker: str):
+ def _format_reference_label(self, marker: str):
if marker in self.references:
- return f"{self.edition}-{self.chapter}.{self.section}.{self.references[marker]}", True
+ 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 marker, False
+ return None
def render_dash(self, token):
return " – "
@@ -222,47 +210,11 @@ 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):
+ def render_reference(self, token: Reference):
# Look up the figure number from the map
- figure_num = self.figure_map.get(token.first, "?")
+ label = self._format_reference_label(token.marker) or "?"
return (
- f'{figure_num}"
+ f'{label}'
)
def render_question(self, token):
@@ -282,31 +234,34 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
return (
f'\n'
f' \n'
- f" Abbildung {self.get_reference_label(marker)}: {text}\n"
+ f" Abbildung {label}: {text}\n"
"\n"
)
- def render_picture(self, token):
- 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, token.label, alt_text)
-
@staticmethod
- def render_photo_helper(id, ref, text, number, alt_text):
+ def render_photo_helper(id, ref, text, label, alt_text):
return f"""
- Abbildung {number}: {text}
+ Abbildung {label}: {text}
"""
- def render_photo(self, token):
- 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.ref, token.text, token.number, alt_text)
+ 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
@@ -317,10 +272,11 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
table = f'
\n{self.render_inner(token)}'
if token.caption != "":
- # Include hierarchical number in caption if available
+ # Include hierarchical number in caption if the table is named.
caption_text = token.caption
- if hasattr(token, "number") and token.number:
- caption_text = f"Tabelle {token.number}: {token.caption}"
+ label = self._format_reference_label(token.name)
+ if label:
+ caption_text = f"Tabelle {label}: {token.caption}"
table += f"
{caption_text}
\n"
table += "
"
diff --git a/renderer/fifty_ohm_latex_renderer.py b/renderer/fifty_ohm_latex_renderer.py
index 86989ca..39c9c96 100644
--- a/renderer/fifty_ohm_latex_renderer.py
+++ b/renderer/fifty_ohm_latex_renderer.py
@@ -3,10 +3,9 @@ from mistletoe.latex_renderer import LaTeXRenderer
from .comment import BlockComment
from .dash import Dash
from .halfwidth_spaces import HalfwidthSpaces
+from .image import Image
from .index import Index
from .nonbreaking_spaces import NonbreakingSpaces, NonbreakingSpacesDots
-from .photo import Photo
-from .picture import Picture
from .question import Question
from .quote import Quote
from .table import Table, TableBody, TableCell, TableHeader, TableRow
@@ -26,8 +25,7 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer):
NonbreakingSpaces,
NonbreakingSpacesDots,
Question,
- Picture,
- Photo,
+ Image,
Table,
TableBody,
TableRow,
@@ -115,17 +113,18 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer):
\captionof{{figure}}{{{text}}}
\label{{{ref}}}"""
- def render_picture(self, token):
- return self.render_picture_helper(token.id, token.ref, token.text, token.number)
-
@staticmethod
def render_photo_helper(id, ref, text, number):
return rf"""\includegraphics[width=1.0\linewidth]{{photo/{id}}}
\captionof{{figure}}{{{text}}}
\label{{{ref}}}"""
- def render_photo(self, token):
- return self.render_photo_helper(token.id, token.ref, token.text, token.number)
+ def render_image(self, token: Image):
+ if token.kind == "photo":
+ return self.render_photo_helper(token.id, token.marker, token.text, token.label)
+ if token.kind == "picture":
+ return self.render_picture_helper(token.id, token.marker, token.text, token.label)
+ return ""
def render_table(self, token):
table = "\\begin{DARCtabular}" + f"{self.render_inner(token)}" + "\\end{DARCtabular}"
diff --git a/renderer/image.py b/renderer/image.py
new file mode 100644
index 0000000..a2f0691
--- /dev/null
+++ b/renderer/image.py
@@ -0,0 +1,40 @@
+import re
+
+from mistletoe.block_tokenizer import FileWrapper
+
+from renderer.referenced_token import ReferencedToken
+
+
+class Image(ReferencedToken):
+ """
+ Block token for pictures and photos.
+ Examples:
+ [picture:727:amplitude_periode_halbwellen:Positive und negative Halbwellen einer Sinusschwingung]
+ [photo:123:n_wasserfalldiagramm_trx_collage:Ansichten verschiedener Transceiver]
+ """
+
+ pattern = re.compile(r"^\[(?Ppicture|photo):(?P\d+):(?P[^:\]]+):(?P[^:\]]+)\]$")
+
+ @classmethod
+ def start(cls, line: str):
+ return cls.pattern.match(line.strip()) is not None
+
+ @classmethod
+ def check_interrupts_paragraph(cls, lines: FileWrapper):
+ line = lines.peek()
+ if line is not None:
+ return cls.start(line)
+ else:
+ return False
+
+ @classmethod
+ def read(cls, lines: FileWrapper):
+ match = cls.pattern.match(next(lines).strip())
+ return match.group("kind"), match.group("id"), match.group("marker"), match.group("text")
+
+ def __init__(self, match):
+ kind, id, marker, text = match
+ super().__init__(marker)
+ self.kind = kind
+ self.id = id
+ self.text = text
diff --git a/renderer/photo.py b/renderer/photo.py
deleted file mode 100644
index 5634372..0000000
--- a/renderer/photo.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import re
-
-from mistletoe.span_token import SpanToken
-
-
-class Photo(SpanToken):
- """
- Span token for photos.
- Example: [photo:123:n_wasserfalldiagramm_trx_collage:Ansichten verschiedener Transceiver]
- """
-
- parse_inner = False
- pattern = re.compile(r"\[photo:(\d+):([^:\]]+):([^:\]]+)\]")
-
- def __init__(self, match_object):
- self.id = match_object.group(1)
- self.ref = match_object.group(2)
- self.text = match_object.group(3)
- self.number = "TODO"
diff --git a/renderer/picture.py b/renderer/picture.py
deleted file mode 100644
index b645126..0000000
--- a/renderer/picture.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import re
-
-from mistletoe.block_tokenizer import FileWrapper
-
-from renderer.referenced_token import ReferencedToken
-
-
-class Picture(ReferencedToken):
- """
- Span token for pictures.
- Example: [picture:727:amplitude_periode_halbwellen:Positive und negative Halbwellen einer Sinusschwingung]
- """
-
- pattern = re.compile(r"^\[picture:(?P\d+):(?P[^:\]]+):(?P[^:\]]+)\]$")
-
- @classmethod
- def start(cls, line: str):
- return cls.pattern.match(line.strip()) is not None
-
- @classmethod
- def check_interrupts_paragraph(cls, lines: FileWrapper):
- return cls.pattern.match(lines.peek())
-
- @classmethod
- def read(cls, lines: FileWrapper):
- match = cls.pattern.match(next(lines))
- return match.group("id"), match.group("marker"), match.group("text")
-
- def __init__(self, match):
- id, marker, text = match
- super().__init__(marker)
- self.id = id
- self.text = text
diff --git a/renderer/references.py b/renderer/reference.py
similarity index 73%
rename from renderer/references.py
rename to renderer/reference.py
index bb9de72..d1f7287 100644
--- a/renderer/references.py
+++ b/renderer/reference.py
@@ -3,7 +3,7 @@ import re
from mistletoe.span_token import SpanToken
-class References(SpanToken):
+class Reference(SpanToken):
"""
Resolve references for html
Identifies "[ref:$]"
@@ -12,4 +12,4 @@ class References(SpanToken):
pattern = re.compile(r"\[ref\:(\S+)\]")
def __init__(self, match_object):
- self.first = match_object.group(1)
+ self.marker: str = match_object.group(1)
diff --git a/renderer/referenced_token.py b/renderer/referenced_token.py
index 1f07b46..7a1e0d8 100644
--- a/renderer/referenced_token.py
+++ b/renderer/referenced_token.py
@@ -3,15 +3,16 @@ from mistletoe.block_token import BlockToken
class ReferencedToken(BlockToken):
- line_number: int = -1
- marker: str = None
- label: str = None
-
def __init__(self, marker: str):
- self.marker = marker
-
- if marker not in mistletoe.token._root_node.references:
- self.label = len(mistletoe.token._root_node.references) + 1
- mistletoe.token._root_node.references[marker] = self.label
+ if marker != "":
+ self.marker = marker
+ references = mistletoe.token._root_node.references
+ if marker not in references:
+ label = str(len(references) + 1)
+ references[marker] = label
+ else:
+ label = references[marker]
+ self.label = label
else:
- self.label = mistletoe._root_node.references[marker]
+ self.marker = None
+ self.label = None
diff --git a/renderer/table.py b/renderer/table.py
index 7a107e6..45135e5 100644
--- a/renderer/table.py
+++ b/renderer/table.py
@@ -4,6 +4,8 @@ from enum import Enum
from mistletoe import span_token
from mistletoe.block_token import BlockToken
+from renderer.referenced_token import ReferencedToken
+
class CellAlignment(Enum):
LEFT = "l"
@@ -76,7 +78,7 @@ class TableBody(BlockToken):
self.children, self.header = rows, header
-class Table(BlockToken):
+class Table(ReferencedToken):
name: str
caption: str
header: list[TableHeader]
@@ -115,4 +117,8 @@ class Table(BlockToken):
return children, name, caption
def __init__(self, match):
- self.children, self.name, self.caption = match
+ children, name, caption = match
+ super().__init__(name)
+ self.children = children
+ self.name = name
+ self.caption = caption
diff --git a/src/build.py b/src/build.py
index 7803885..d243be4 100644
--- a/src/build.py
+++ b/src/build.py
@@ -7,10 +7,10 @@ import zipfile
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
-from mistletoe import Document
from rich.progress import BarColumn, Progress, TaskProgressColumn, TextColumn, TimeRemainingColumn
from tqdm import tqdm
+from renderer.document import Document
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
from renderer.index import Index
@@ -222,12 +222,7 @@ class Build:
section=section_num,
section_url=section_filename,
) as renderer:
- # First pass: collect all figures and assign hierarchical numbers
- doc = Document(section["content"])
- renderer.collect_figures(doc)
-
- # Second pass: render with hierarchical numbers
- section["content"] = renderer.render(doc)
+ section["content"] = renderer.render(Document(section["content"]))
result = section_template.render(
edition=edition,
@@ -266,58 +261,51 @@ class Build:
# Note: filename uses original edition (e.g., "N"), not modified edition with "S" (e.g., "NS")
slide_filename = f"{edition}_slide_{chapter['ident']}.html"
- with FiftyOhmHtmlSlideRenderer(
- question_renderer=self.__build_question_slide,
- picture_handler=self.__picture_handler,
- photo_handler=self.__photo_handler,
- include_handler=self.__include_handler,
- edition=edition,
- chapter=chapter_num,
- section="0", # Will be updated per section
- section_url=slide_filename,
- ) as renderer:
- result = "\n"
- result += f'\n
{chapter["title"]}
\n\n'
- result += help_template.render()
- result += "\n"
+ result = "\n"
+ result += f'\n
{chapter["title"]}
\n\n'
+ result += help_template.render()
+ result += "\n"
- section_counter = 0
- slides_task = progress.add_task("Rendering slides ...")
- for section in progress.track(sections, task_id=slides_task):
- progress.update(slides_task, description=f"Rendering slides of {section['title']}")
+ section_counter = 0
+ slides_task = progress.add_task("Rendering slides ...")
+ for section in progress.track(sections, task_id=slides_task):
+ progress.update(slides_task, description=f"Rendering slides of {section['title']}")
- if section["slide"] is None:
- continue
+ if section["slide"] is None:
+ continue
- section_counter += 1
- # Update section number for this section
- renderer.section = str(section_counter)
- # Reset figure counter for each section
- renderer.figure_counter = 0
- renderer.figure_map = {}
+ section_counter += 1
- if not section["slide"].startswith("---"):
- section["slide"] = "---\n" + section["slide"]
+ if not section["slide"].startswith("---"):
+ section["slide"] = "---\n" + section["slide"]
- # First pass: collect figures
- doc = Document(section["slide"])
- renderer.collect_figures(doc)
-
- # Second pass: render with hierarchical numbers
- tmp = f'\n
{section["title"]}
\n\n'
- tmp += renderer.render(doc)
- result += f"{tmp}\n"
-
- progress.remove_task(slides_task)
-
- result += next_template.render(
+ # A fresh renderer per section keeps the figure counter and
+ # reference map scoped to that section.
+ with FiftyOhmHtmlSlideRenderer(
+ question_renderer=self.__build_question_slide,
+ picture_handler=self.__picture_handler,
+ photo_handler=self.__photo_handler,
+ include_handler=self.__include_handler,
edition=edition,
- next_chapter=next_chapter,
- chapter=chapter,
- )
+ chapter=chapter_num,
+ section=str(section_counter),
+ section_url=slide_filename,
+ ) as renderer:
+ tmp = f'\n
\n"
+
+
+def render_html(markdown):
+ """Render markdown with the HTML renderer through the project's Document."""
+ with FiftyOhmHtmlRenderer() as renderer:
+ return renderer.render(Document(markdown))
+
+
+def render_slide(markdown):
+ """Render markdown with the slide renderer through the project's Document."""
+ with FiftyOhmHtmlSlideRenderer() as renderer:
+ return renderer.render(Document(markdown))