Merge pull request #114 from DARC-e-V/index-search
feature/Index search
This commit is contained in:
@@ -15,4 +15,5 @@ bd.build_edition("NEA")
|
|||||||
bd.build_assets()
|
bd.build_assets()
|
||||||
bd.build_solutions()
|
bd.build_solutions()
|
||||||
bd.build_question_index()
|
bd.build_question_index()
|
||||||
|
bd.build_index()
|
||||||
bd.build_zip()
|
bd.build_zip()
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ def main(
|
|||||||
# Copy assets to build folder
|
# Copy assets to build folder
|
||||||
bd.build_assets()
|
bd.build_assets()
|
||||||
bd.build_question_index()
|
bd.build_question_index()
|
||||||
|
bd.build_index()
|
||||||
|
|
||||||
if build_zip:
|
if build_zip:
|
||||||
# Build zip file of output
|
# Build zip file of output
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
from mistletoe import Document, HtmlRenderer
|
from mistletoe import Document, HtmlRenderer
|
||||||
|
|
||||||
@@ -90,6 +92,9 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
|||||||
# Single unified counter for all figure types (pictures, photos, tables)
|
# Single unified counter for all figure types (pictures, photos, tables)
|
||||||
self.figure_counter = 0
|
self.figure_counter = 0
|
||||||
|
|
||||||
|
# Keep track of index ids already emitted in this document.
|
||||||
|
self.index_anchor_ids = set()
|
||||||
|
|
||||||
def render_dash(self, token):
|
def render_dash(self, token):
|
||||||
return " – "
|
return " – "
|
||||||
|
|
||||||
@@ -335,6 +340,24 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
|||||||
def render_formula(self, token):
|
def render_formula(self, token):
|
||||||
return f"\n$${token.formula}$$\n"
|
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):
|
def render_index(self, token):
|
||||||
# For HTML just ignore the index
|
first = self._normalize_index_part(token.first)
|
||||||
return ""
|
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>'
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from tqdm import tqdm
|
|||||||
|
|
||||||
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
|
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
|
||||||
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
|
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
|
||||||
|
from renderer.index import Index
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
@@ -28,6 +29,9 @@ class Build:
|
|||||||
self.question_index = {}
|
self.question_index = {}
|
||||||
self.question_token_pattern = re.compile(r"^\s*\[question:([\w\d]+)\]", re.MULTILINE)
|
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):
|
def __parse_katalog(self):
|
||||||
with self.config.p_data_fragenkatalog.open() as file:
|
with self.config.p_data_fragenkatalog.open() as file:
|
||||||
fragenkatalog = json.load(file)
|
fragenkatalog = json.load(file)
|
||||||
@@ -402,6 +406,13 @@ class Build:
|
|||||||
section["title"],
|
section["title"],
|
||||||
section["content"],
|
section["content"],
|
||||||
)
|
)
|
||||||
|
self.__collect_index_occurrences(
|
||||||
|
edition,
|
||||||
|
chapter["title"],
|
||||||
|
ident,
|
||||||
|
section["title"],
|
||||||
|
section["content"],
|
||||||
|
)
|
||||||
|
|
||||||
next_section = (
|
next_section = (
|
||||||
chapter["sections"][section_number] if section_number < len(chapter["sections"]) else None
|
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:
|
def __has_solution(self, question_number: str) -> bool:
|
||||||
return (self.config.p_data_solutions / f"{question_number}.md").exists()
|
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):
|
def build_question_index(self):
|
||||||
|
tqdm.write("Creating question index")
|
||||||
for question_data in self.question_index.values():
|
for question_data in self.question_index.values():
|
||||||
question_data["editions"] = sorted(question_data["editions"])
|
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)
|
json.dump(self.question_index, file, ensure_ascii=False, indent=2, sort_keys=True)
|
||||||
file.write("\n")
|
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:
|
def build_zip(self, zip_name: str | None = None) -> Path:
|
||||||
"""Create a zip archive of the complete build output directory.
|
"""Create a zip archive of the complete build output directory.
|
||||||
|
|
||||||
|
|||||||
+30
-5
@@ -9,11 +9,36 @@ from test.util import paragraph
|
|||||||
@pytest.mark.html
|
@pytest.mark.html
|
||||||
def test_index_html():
|
def test_index_html():
|
||||||
assertions = {
|
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[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501
|
"Im Betrieb werden Yagi-Antennen"
|
||||||
"Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht",
|
+ '<span id="index_antenne__yagi_antenne" class="index-anchor" aria-hidden="true"></span>'
|
||||||
"Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht",
|
+ " 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"
|
||||||
|
+ '<span id="index_antenne__yagi_antenne" class="index-anchor" aria-hidden="true"></span>'
|
||||||
|
+ " oft gedreht"
|
||||||
|
),
|
||||||
|
"Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": (
|
||||||
|
"Im Betrieb werden Yagi-Antennen"
|
||||||
|
+ '<span id="index_antenne" class="index-anchor" aria-hidden="true"></span>'
|
||||||
|
+ " oft gedreht"
|
||||||
|
),
|
||||||
|
"Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": (
|
||||||
|
"Im Betrieb werden Yagi-Antennen"
|
||||||
|
+ '<span id="index_antenne" class="index-anchor" aria-hidden="true"></span>'
|
||||||
|
+ " oft gedreht"
|
||||||
|
),
|
||||||
|
"Im Betrieb werden Yagi-Antennen[index:Test Antenne] oft gedreht": (
|
||||||
|
"Im Betrieb werden Yagi-Antennen"
|
||||||
|
+ '<span id="index_test_antenne" class="index-anchor" aria-hidden="true"></span>'
|
||||||
|
+ " oft gedreht"
|
||||||
|
),
|
||||||
|
"Im Betrieb [index:Antenne] und nochmals [index:Antenne] oft gedreht": (
|
||||||
|
"Im Betrieb"
|
||||||
|
+ '<span id="index_antenne" class="index-anchor" aria-hidden="true"></span>'
|
||||||
|
+ " und nochmals oft gedreht"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value in assertions.items():
|
for key, value in assertions.items():
|
||||||
|
|||||||
Reference in New Issue
Block a user