diff --git a/build.py b/build.py
index bfb2ae0..a8f4c68 100644
--- a/build.py
+++ b/build.py
@@ -15,4 +15,5 @@ bd.build_edition("NEA")
bd.build_assets()
bd.build_solutions()
bd.build_question_index()
+bd.build_index()
bd.build_zip()
diff --git a/main.py b/main.py
index 5b83092..51bf833 100644
--- a/main.py
+++ b/main.py
@@ -43,6 +43,7 @@ def main(
# Copy assets to build folder
bd.build_assets()
bd.build_question_index()
+ bd.build_index()
if build_zip:
# Build zip file of output
diff --git a/renderer/fifty_ohm_html_renderer.py b/renderer/fifty_ohm_html_renderer.py
index 2e316bd..8817d14 100644
--- a/renderer/fifty_ohm_html_renderer.py
+++ b/renderer/fifty_ohm_html_renderer.py
@@ -1,3 +1,5 @@
+import re
+
from jinja2 import Environment, FileSystemLoader
from mistletoe import Document, HtmlRenderer
@@ -90,6 +92,9 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
# Single unified counter for all figure types (pictures, photos, tables)
self.figure_counter = 0
+ # Keep track of index ids already emitted in this document.
+ self.index_anchor_ids = set()
+
def render_dash(self, token):
return " – "
@@ -335,6 +340,24 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
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):
- # For HTML just ignore the index
- return ""
+ 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''
diff --git a/src/build.py b/src/build.py
index b3edc00..7803885 100644
--- a/src/build.py
+++ b/src/build.py
@@ -13,6 +13,7 @@ from tqdm import tqdm
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
+from renderer.index import Index
from .config import Config
@@ -28,6 +29,9 @@ class Build:
self.question_index = {}
self.question_token_pattern = re.compile(r"^\s*\[question:([\w\d]+)\]", re.MULTILINE)
+ self.index_entries = {}
+ self.index_token_pattern = Index.pattern
+
def __parse_katalog(self):
with self.config.p_data_fragenkatalog.open() as file:
fragenkatalog = json.load(file)
@@ -402,6 +406,13 @@ class Build:
section["title"],
section["content"],
)
+ self.__collect_index_occurrences(
+ edition,
+ chapter["title"],
+ ident,
+ section["title"],
+ section["content"],
+ )
next_section = (
chapter["sections"][section_number] if section_number < len(chapter["sections"]) else None
@@ -556,7 +567,43 @@ class Build:
def __has_solution(self, question_number: str) -> bool:
return (self.config.p_data_solutions / f"{question_number}.md").exists()
+ def __collect_index_occurrences(
+ self,
+ edition: str,
+ chapter_title: str,
+ section_ident: str,
+ section_title: str,
+ section_content: str,
+ ):
+ for match in self.index_token_pattern.finditer(section_content or ""):
+ first = match.group(1).strip()
+ second = match.group(2).strip() if match.group(2) else None
+
+ normalized_first = FiftyOhmHtmlRenderer._normalize_index_part(first)
+ if second:
+ normalized_second = FiftyOhmHtmlRenderer._normalize_index_part(second)
+ anchor_id = f"index_{normalized_first}__{normalized_second}"
+ else:
+ anchor_id = f"index_{normalized_first}"
+
+ entry_key = f"{section_ident}#{anchor_id}"
+ index_entry = self.index_entries.setdefault(
+ entry_key,
+ {
+ "term": first,
+ "subterm": second,
+ "anchor_id": anchor_id,
+ "section": section_ident,
+ "chapter_title": chapter_title,
+ "section_title": section_title,
+ "editions": [],
+ },
+ )
+ if edition not in index_entry["editions"]:
+ index_entry["editions"].append(edition)
+
def build_question_index(self):
+ tqdm.write("Creating question index")
for question_data in self.question_index.values():
question_data["editions"] = sorted(question_data["editions"])
@@ -565,6 +612,26 @@ class Build:
json.dump(self.question_index, file, ensure_ascii=False, indent=2, sort_keys=True)
file.write("\n")
+ def build_index(self):
+ tqdm.write("Creating index")
+ for index_data in self.index_entries.values():
+ index_data["editions"] = sorted(index_data["editions"])
+
+ entries = sorted(
+ self.index_entries.values(),
+ key=lambda item: (
+ item["term"].casefold(),
+ (item["subterm"] or "").casefold(),
+ item["section"],
+ item["anchor_id"],
+ ),
+ )
+
+ path = self.config.p_build_assets / "index.json"
+ with (path).open("w", encoding="utf-8") as file:
+ json.dump(entries, file, ensure_ascii=False, indent=2)
+ file.write("\n")
+
def build_zip(self, zip_name: str | None = None) -> Path:
"""Create a zip archive of the complete build output directory.
diff --git a/test/test_index.py b/test/test_index.py
index 2c2e74b..ec538fd 100644
--- a/test/test_index.py
+++ b/test/test_index.py
@@ -9,11 +9,36 @@ from test.util import paragraph
@pytest.mark.html
def test_index_html():
assertions = {
- "Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501
- "Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501
- "Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht",
- "Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht",
- "Im Betrieb werden Yagi-Antennen[index:Test Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501
+ "Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": (
+ "Im Betrieb werden Yagi-Antennen"
+ + ''
+ + " oft gedreht"
+ ),
+ "Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": (
+ "Im Betrieb werden Yagi-Antennen"
+ + ''
+ + " oft gedreht"
+ ),
+ "Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": (
+ "Im Betrieb werden Yagi-Antennen"
+ + ''
+ + " oft gedreht"
+ ),
+ "Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": (
+ "Im Betrieb werden Yagi-Antennen"
+ + ''
+ + " oft gedreht"
+ ),
+ "Im Betrieb werden Yagi-Antennen[index:Test Antenne] oft gedreht": (
+ "Im Betrieb werden Yagi-Antennen"
+ + ''
+ + " oft gedreht"
+ ),
+ "Im Betrieb [index:Antenne] und nochmals [index:Antenne] oft gedreht": (
+ "Im Betrieb"
+ + ''
+ + " und nochmals oft gedreht"
+ ),
}
for key, value in assertions.items():