diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py
index 8cb9e66..9d3a746 100644
--- a/renderer/fifty_ohm_html_renderer.py
+++ b/renderer/fifty_ohm_html_renderer.py
@@ -28,7 +28,16 @@ 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,
+ **kwargs,
):
super().__init__(
Dash,
@@ -61,6 +70,17 @@ 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
+
+ # 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 " – "
@@ -175,10 +195,56 @@ 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 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'{self.ref_id}"
+ f" onclick=\"highlightRef('{token.first}');\">{figure_num}"
)
def render_question(self, token):
@@ -222,10 +288,19 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
return self.render_photo_helper(token.id, token.ref, token.text, token.number)
def render_table(self, token: Table):
- table = f'
\n{self.render_inner(token)}'
+ # 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 != "":
- table += f"{token.caption}\n"
+ # 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_text}\n"
table += "
"
diff --git a/renderer/fifty_ohm_html_slide_renderer.py b/renderer/fifty_ohm_html_slide_renderer.py
index 8ded848..f867962 100644
--- a/renderer/fifty_ohm_html_slide_renderer.py
+++ b/renderer/fifty_ohm_html_slide_renderer.py
@@ -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)
diff --git a/src/build.py b/src/build.py
index 26c8964..c5f64b5 100644
--- a/src/build.py
+++ b/src/build.py
@@ -161,19 +161,39 @@ class Build:
# cached
def __build_section(
- self, edition, edition_name, section, section_id, chapter, next_section=None, next_chapter=None
+ self,
+ edition,
+ edition_name,
+ section,
+ section_id,
+ chapter,
+ next_section=None,
+ next_chapter=None,
+ chapter_number=None,
):
section_template = self.env.get_template("html/section.html")
next_section_template = self.env.get_template("html/next_section.html")
next_chapter_template = self.env.get_template("html/next_chapter.html")
with (self.config.p_build / f"{edition}_{section['ident']}.html").open("w") as file:
+ # Use provided chapter_number or fall back to chapter dict
+ chapter_num = str(chapter_number) if chapter_number is not None else chapter.get("number", "0")
+ section_num = str(section_id)
+
with FiftyOhmHtmlRenderer(
question_renderer=self.__build_question,
picture_handler=self.__picture_handler,
photo_handler=self.__photo_handler,
include_handler=self.__include_handler,
+ edition=edition,
+ chapter=chapter_num,
+ section=section_num,
) as renderer:
- section["content"] = renderer.render(Document(section["content"]))
+ # 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)
result = section_template.render(
edition=edition,
@@ -197,30 +217,53 @@ class Build:
result = self.__build_page(result, course_wrapper=True)
file.write(result)
- def __build_chapter_slidedeck(self, edition, chapter, sections, next_chapter):
+ def __build_chapter_slidedeck(self, edition, chapter, sections, next_chapter, chapter_number=None):
with (self.config.p_build / f"{edition}_slide_{chapter['ident']}.html").open("w") as file:
slide_template = self.env.get_template("slide/slide.html")
help_template = self.env.get_template("slide/help.html")
next_template = self.env.get_template("slide/next.html")
+
+ # Parse chapter number
+ chapter_num = chapter.get("number", "0")
+
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
) as renderer:
result = "\n"
result += f'\n'
result += help_template.render()
result += "\n"
+
+ section_counter = 0
for section in sections:
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 = {}
+
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'
- tmp += renderer.render(Document(section["slide"]))
+ tmp += renderer.render(doc)
result += f"\n"
+
result += next_template.render(
edition=edition,
next_chapter=next_chapter,
@@ -271,15 +314,26 @@ class Build:
edition_name = book["title"]
self.__build_book_index(book)
self.__build_slide_index(book)
- for number, chapter in enumerate(tqdm(chapters, desc=f"Build Edition: {edition}"), 1):
- next_chapter = chapters[number] if number < len(chapters) else None
- self.__build_chapter(edition, edition_name, number, chapter, next_chapter)
- self.__build_chapter_slidedeck(edition, chapter, chapter["sections"], next_chapter)
+ for chapter_number, chapter in enumerate(tqdm(chapters, desc=f"Build Edition: {edition}"), 1):
+ next_chapter = chapters[chapter_number] if chapter_number < len(chapters) else None
+ self.__build_chapter(edition, edition_name, chapter_number, chapter, next_chapter)
+ self.__build_chapter_slidedeck(edition, chapter, chapter["sections"], next_chapter, chapter_number)
- for i, section in enumerate(chapter["sections"], 1):
+ for section_number, section in enumerate(chapter["sections"], 1):
tqdm.write(f"Rendering section {section['title']}")
- next_section = chapter["sections"][i] if i < len(chapter["sections"]) else None
- self.__build_section(edition, edition_name, section, i, chapter, next_section, next_chapter)
+ next_section = (
+ chapter["sections"][section_number] if section_number < len(chapter["sections"]) else None
+ )
+ self.__build_section(
+ edition,
+ edition_name,
+ section,
+ section_number,
+ chapter,
+ next_section,
+ next_chapter,
+ chapter_number,
+ )
def build_assets(self):
self.config.p_build.mkdir(exist_ok=True)
diff --git a/test/test_hierarchical_numbering.py b/test/test_hierarchical_numbering.py
new file mode 100644
index 0000000..2974182
--- /dev/null
+++ b/test/test_hierarchical_numbering.py
@@ -0,0 +1,222 @@
+import mistletoe
+import pytest
+
+from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
+from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
+
+
+@pytest.mark.html
+def test_hierarchical_picture_numbering():
+ """Test that pictures get hierarchical numbers in format edition-chapter.section.counter"""
+ markdown_text = """
+[picture:123:pic1:First picture]
+
+Some text here.
+
+[picture:456:pic2:Second picture]
+
+Reference to first: [ref:pic1]
+Reference to second: [ref:pic2]
+"""
+
+ renderer = FiftyOhmHtmlRenderer(edition="E", chapter="2", section="3")
+ doc = mistletoe.Document(markdown_text)
+
+ # First pass: collect figures
+ renderer.collect_figures(doc)
+
+ # Verify figure map
+ assert renderer.figure_map["pic1"] == "E-2.3.1"
+ assert renderer.figure_map["pic2"] == "E-2.3.2"
+
+ # Second pass: render
+ result = renderer.render(doc)
+
+ # Verify hierarchical numbers in output
+ assert "Abbildung E-2.3.1:" in result
+ assert "Abbildung E-2.3.2:" in result
+
+ # Verify references are resolved
+ assert ">E-2.3.1" in result
+ assert ">E-2.3.2" in result
+
+
+@pytest.mark.html
+def test_hierarchical_photo_numbering():
+ """Test that photos get hierarchical numbers"""
+ markdown_text = """
+[photo:789:photo1:First photo]
+
+[photo:101:photo2:Second photo]
+
+Reference: [ref:photo1]
+"""
+
+ renderer = FiftyOhmHtmlRenderer(edition="N", chapter="1", section="2")
+ doc = mistletoe.Document(markdown_text)
+
+ renderer.collect_figures(doc)
+
+ assert renderer.figure_map["photo1"] == "N-1.2.1"
+ assert renderer.figure_map["photo2"] == "N-1.2.2"
+
+ result = renderer.render(doc)
+
+ assert "Abbildung N-1.2.1:" in result
+ assert "Abbildung N-1.2.2:" in result
+ assert ">N-1.2.1" in result
+
+
+@pytest.mark.html
+def test_hierarchical_table_numbering():
+ """Test that tables with names get hierarchical numbers"""
+ markdown_text = """
+| Header 1 | Header 2 |
+| Data 1 | Data 2 |
+[table:table1:First table]
+
+| Col A | Col B |
+| Val 1 | Val 2 |
+[table:table2:Second table]
+
+Reference: [ref:table1]
+"""
+
+ renderer = FiftyOhmHtmlRenderer(edition="A", chapter="3", section="1")
+ doc = mistletoe.Document(markdown_text)
+
+ renderer.collect_figures(doc)
+
+ assert renderer.figure_map["table1"] == "A-3.1.1"
+ assert renderer.figure_map["table2"] == "A-3.1.2"
+
+ result = renderer.render(doc)
+
+ assert "Tabelle A-3.1.1:" in result
+ assert "Tabelle A-3.1.2:" in result
+ assert ">A-3.1.1" in result
+
+
+@pytest.mark.html
+def test_mixed_figure_types_share_counter():
+ """Test that pictures, photos, and tables share a single unified counter"""
+ markdown_text = """
+[picture:1:pic1:Picture one]
+
+[photo:2:photo1:Photo one]
+
+| A | B |
+| 1 | 2 |
+[table:tab1:Table one]
+
+[picture:3:pic2:Picture two]
+
+References: [ref:pic1], [ref:photo1], [ref:tab1], [ref:pic2]
+"""
+
+ renderer = FiftyOhmHtmlRenderer(edition="E", chapter="5", section="2")
+ doc = mistletoe.Document(markdown_text)
+
+ renderer.collect_figures(doc)
+
+ # All types share the same counter
+ assert renderer.figure_map["pic1"] == "E-5.2.1"
+ assert renderer.figure_map["photo1"] == "E-5.2.2"
+ assert renderer.figure_map["tab1"] == "E-5.2.3"
+ assert renderer.figure_map["pic2"] == "E-5.2.4"
+
+ result = renderer.render(doc)
+
+ # Verify all references are resolved with correct numbers
+ assert "Abbildung E-5.2.1:" in result # pic1
+ assert "Abbildung E-5.2.2:" in result # photo1
+ assert "Tabelle E-5.2.3:" in result # tab1
+ assert "Abbildung E-5.2.4:" in result # pic2
+
+
+@pytest.mark.slide
+def test_slide_renderer_adds_s_suffix():
+ """Test that slide renderer adds 'S' suffix to edition"""
+ markdown_text = """
+[picture:1:pic1:Slide picture]
+
+Reference: [ref:pic1]
+"""
+
+ renderer = FiftyOhmHtmlSlideRenderer(edition="E", chapter="2", section="1")
+ doc = mistletoe.Document(markdown_text)
+
+ # Verify edition has S suffix
+ assert renderer.edition == "ES"
+
+ renderer.collect_figures(doc)
+
+ # Verify hierarchical number includes S
+ assert renderer.figure_map["pic1"] == "ES-2.1.1"
+
+ result = renderer.render(doc)
+
+ assert "Abbildung ES-2.1.1:" in result
+ assert ">ES-2.1.1" in result
+
+
+@pytest.mark.html
+def test_fallback_numbering_without_context():
+ """Test that numbering falls back to simple counter when context is missing"""
+ markdown_text = """
+[picture:1:pic1:Picture]
+
+Reference: [ref:pic1]
+"""
+
+ # No edition/chapter/section provided
+ renderer = FiftyOhmHtmlRenderer()
+ doc = mistletoe.Document(markdown_text)
+
+ renderer.collect_figures(doc)
+
+ # Should fall back to simple counter
+ assert renderer.figure_map["pic1"] == "1"
+
+ result = renderer.render(doc)
+
+ assert "Abbildung 1:" in result
+ assert ">1" in result
+
+
+@pytest.mark.html
+def test_unresolved_reference():
+ """Test that references to non-existent figures show '?'"""
+ markdown_text = """
+Reference to non-existent: [ref:nonexistent]
+"""
+
+ renderer = FiftyOhmHtmlRenderer(edition="E", chapter="1", section="1")
+ doc = mistletoe.Document(markdown_text)
+
+ renderer.collect_figures(doc)
+
+ result = renderer.render(doc)
+
+ # Should show ? for unresolved reference
+ assert ">?" in result
+
+
+@pytest.mark.html
+def test_table_without_name_not_numbered():
+ """Test that tables without names are not numbered"""
+ markdown_text = """
+| Header 1 | Header 2 |
+| Data 1 | Data 2 |
+
+[picture:1:pic1:Picture one]
+"""
+
+ renderer = FiftyOhmHtmlRenderer(edition="E", chapter="1", section="1")
+ doc = mistletoe.Document(markdown_text)
+
+ renderer.collect_figures(doc)
+
+ # Only the picture should be numbered
+ assert renderer.figure_map["pic1"] == "E-1.1.1"
+ assert renderer.figure_counter == 1
diff --git a/test/test_references.py b/test/test_references.py
index ab332b4..5011685 100644
--- a/test/test_references.py
+++ b/test/test_references.py
@@ -7,12 +7,11 @@ from test.util import paragraph
@pytest.mark.html
def test_references_html():
- # Right now this tests only the default values.
- # The correct behavior can be tested when the information is added to the renderer class
+ # References without figures in the map should show "?"
assertions = {
- "[ref:e_oszilloskop_bildschirmfoto_sinus]": '0', # noqa: E501
- "[ref:n_rst_r]": '0',
- "[ref:Uuuuuh]": '0', # Test the default parameters # noqa: E501
+ "[ref:e_oszilloskop_bildschirmfoto_sinus]": '?', # noqa: E501
+ "[ref:n_rst_r]": '?',
+ "[ref:Uuuuuh]": '?',
}
for key, value in assertions.items():