from __future__ import annotations from pathlib import Path import shutil import os import json import re import markdown import struct from urllib.parse import parse_qs, quote_plus, urlparse import settings_config import validate from jinja2 import Environment, FileSystemLoader ROOT_DIR = Path(__file__).resolve().parent.parent CONTENT_DIR = settings_config.active_content_dir() OUTPUT_DIR = ROOT_DIR / "ausgabe" GENERATOR_DIR = ROOT_DIR / "generator" TEMPLATE_DIR = GENERATOR_DIR / "templates" STATIC_DIR = GENERATOR_DIR / "static" COMPONENTS_FILE = GENERATOR_DIR / "components.json" SETTINGS_FILE = GENERATOR_DIR / "settings.json" SETTINGS_MARKDOWN_FILE = settings_config.active_settings_file() or settings_config.SETTINGS_MARKDOWN_FILE MEDIA_DIR_NAME = "medien" LEGACY_MEDIA_DIR_NAME = "assets" GALLERY_DIR_NAME = "galerie" LEGACY_GALLERY_DIR_NAME = "gallery" FOOTER_DIR_NAME = "fusszeile" LEGACY_FOOTER_DIR_NAME = "footer" FOOTER_FILE_NAME = "fusszeile.md" LEGACY_FOOTER_FILE_NAME = "footer.md" IMAGE_EXTENSIONS = { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg" } VIDEO_EXTENSIONS = { ".mp4", ".webm", ".ogg", ".mov" } env = Environment( loader=FileSystemLoader(TEMPLATE_DIR) ) with open(COMPONENTS_FILE, encoding="utf-8") as f: COMPONENTS = json.load(f) DEFAULT_SETTINGS = settings_config.DEFAULT_SETTINGS def merge_settings( defaults: dict, custom: dict ) -> dict: merged = defaults.copy() for key, value in custom.items(): if ( isinstance(value, dict) and isinstance(merged.get(key), dict) ): merged[key] = merge_settings( merged[key], value ) else: merged[key] = value return merged def load_settings() -> dict: return settings_config.load_settings() SETTINGS = DEFAULT_SETTINGS def run_validation(format_settings: bool = True): messages, language = validate.collect_messages(format_settings=format_settings) if not messages: print(validate.tr(language, "ok")) return print(validate.tr(language, "found_header")) for message in messages: print(f"- {message}") print(validate.tr(language, "found_footer")) if validate.has_errors(messages): raise SystemExit(1) def normalize_component(name: str) -> str: return COMPONENTS.get(name, name) def parse_annotation(line: str) -> tuple[str, str | None]: annotation = line.strip()[1:] if ":" in annotation: component, parameter = annotation.split(":", 1) return ( normalize_component(component.strip()), parameter.strip() ) return ( normalize_component(annotation.strip()), None ) def parse_blocks(md_text: str) -> list[dict]: blocks = [] current_type = "text" current_parameter = None current_lines = [] for line in md_text.splitlines(): if line.strip().startswith("@"): if current_lines: blocks.append({ "type": current_type, "parameter": current_parameter, "content": "\n".join(current_lines).strip() }) current_type, current_parameter = parse_annotation(line) current_lines = [] else: current_lines.append(line) if current_lines: blocks.append({ "type": current_type, "parameter": current_parameter, "content": "\n".join(current_lines).strip() }) return [ block for block in blocks if block["content"] ] def render_markdown(md_text: str) -> str: return markdown.markdown( md_text, extensions=["extra"] ) def render_inline_markdown(md_text: str) -> str: html = render_markdown(md_text.strip()) match = re.fullmatch(r"

(.*)

", html, re.S) return match.group(1) if match else html def split_h2_blocks(md_text: str): lines = md_text.splitlines() title_lines = [] blocks = [] current = [] for line in lines: if line.startswith("## "): if current: blocks.append( "\n".join(current).strip() ) current = [line] else: if current: current.append(line) else: title_lines.append(line) if current: blocks.append( "\n".join(current).strip() ) return ( "\n".join(title_lines).strip(), blocks ) def extract_images(md_text: str) -> list[dict]: pattern = r"!\[(.*?)\]\((.*?)\)" images = [] for alt, src in re.findall(pattern, md_text): images.append({ "alt": alt.strip(), "src": src.strip() }) return images def remove_markdown_images(md_text: str) -> str: return re.sub( r"!\[(.*?)\]\((.*?)\)", "", md_text ).strip() def remove_first_image(md_text: str): pattern = r"!\[(.*?)\]\((.*?)\)" match = re.search(pattern, md_text) if not match: return None, md_text image = { "alt": match.group(1).strip(), "src": match.group(2).strip() } remaining = ( md_text[:match.start()] + md_text[match.end():] ) return image, remaining.strip() def extract_links(md_text: str) -> list[dict]: pattern = r"\[(.*?)\]\((.*?)\)" links = [] for label, href in re.findall(pattern, md_text): links.append({ "label": label.strip(), "href": href.strip() }) return links def key_value_lines(md_text: str) -> tuple[dict, str]: values = {} remaining_lines = [] for line in md_text.splitlines(): match = re.match(r"^([A-Za-zÄÖÜäöüß _-]+):\s*(.+)$", line) if match: key = slugify_title(match.group(1)).replace("-", "_") values[key] = match.group(2).strip() else: remaining_lines.append(line) return values, "\n".join(remaining_lines).strip() def href_from_output( current_output_file: Path, target_output_file: Path ) -> str: current_dir = current_output_file.parent relative_href = os.path.relpath( target_output_file, start=current_dir ) return relative_href.replace("\\", "/") def content_path_from_src(src: str) -> Path: return CONTENT_DIR / src def output_path_from_src(src: str) -> Path: return OUTPUT_DIR / src def jpeg_dimensions(file_path: Path) -> tuple[int, int] | None: with file_path.open("rb") as f: if f.read(2) != b"\xff\xd8": return None while True: marker_start = f.read(1) if not marker_start: return None if marker_start != b"\xff": continue marker = f.read(1) while marker == b"\xff": marker = f.read(1) if marker in { b"\xc0", b"\xc1", b"\xc2", b"\xc3", b"\xc5", b"\xc6", b"\xc7", b"\xc9", b"\xca", b"\xcb", b"\xcd", b"\xce", b"\xcf" }: f.read(3) height, width = struct.unpack(">HH", f.read(4)) return width, height segment_length_data = f.read(2) if len(segment_length_data) != 2: return None segment_length = struct.unpack( ">H", segment_length_data )[0] f.seek(segment_length - 2, os.SEEK_CUR) def image_dimensions(file_path: Path) -> tuple[int, int] | None: try: with file_path.open("rb") as f: header = f.read(24) if header.startswith(b"\x89PNG\r\n\x1a\n"): width, height = struct.unpack(">II", header[16:24]) return width, height if header[:6] in {b"GIF87a", b"GIF89a"}: width, height = struct.unpack(" dict: return { "width": 1600, "height": 1000 } def image_size_for_content_relative(src: str) -> dict: src = src.split("?", 1)[0].split("#", 1)[0] if is_external_url(src): return fallback_image_size() file_path = CONTENT_DIR / src if not file_path.exists(): return fallback_image_size() dimensions = image_dimensions(file_path) if not dimensions: return fallback_image_size() width, height = dimensions return { "width": width, "height": height } def is_external_url(src: str) -> bool: return ( src.startswith("http://") or src.startswith("https://") ) def media_src_variants(src: str) -> list[str]: variants = [src] if src.startswith(f"{LEGACY_MEDIA_DIR_NAME}/"): variants.append( f"{MEDIA_DIR_NAME}/{src.removeprefix(f'{LEGACY_MEDIA_DIR_NAME}/')}" ) variants = [ variant.replace( f"{MEDIA_DIR_NAME}/{LEGACY_GALLERY_DIR_NAME}/", f"{MEDIA_DIR_NAME}/{GALLERY_DIR_NAME}/" ) for variant in variants ] return list(dict.fromkeys(variants)) def find_asset_by_name(file_name: str) -> Path | None: media_dirs = [ CONTENT_DIR / MEDIA_DIR_NAME, CONTENT_DIR / LEGACY_MEDIA_DIR_NAME ] existing_dirs = [ media_dir for media_dir in media_dirs if media_dir.exists() ] if not existing_dirs: return None matches = [] for media_dir in existing_dirs: matches.extend([ path for path in media_dir.rglob(file_name) if path.is_file() ]) matches = sorted(matches) if not matches: return None return matches[0] def content_relative_from_src( src: str, source_file: Path | None = None ) -> str: src = src.strip().strip("/") candidates = [] for variant in media_src_variants(src): if source_file: candidates.append(source_file.parent / variant) candidates.append(CONTENT_DIR / variant) for candidate in candidates: candidate = candidate.resolve() if not candidate.exists(): continue try: return candidate.relative_to(CONTENT_DIR).as_posix() except ValueError: pass fallback = find_asset_by_name(Path(src).name) if fallback: return fallback.relative_to(CONTENT_DIR).as_posix() return src def image_src_for_page( src: str, current_output_file: Path, source_file: Path | None = None ) -> str: content_relative = content_relative_from_src( src, source_file ) target_output_file = output_path_from_src( content_relative ) return href_from_output( current_output_file, target_output_file ) def collect_gallery_images( folder_name: str, current_output_file: Path ) -> list[dict]: source_folder = CONTENT_DIR / MEDIA_DIR_NAME / GALLERY_DIR_NAME / folder_name if not source_folder.exists(): source_folder = ( CONTENT_DIR / LEGACY_MEDIA_DIR_NAME / LEGACY_GALLERY_DIR_NAME / folder_name ) if not source_folder.exists(): return [] images = [] for file in sorted(source_folder.iterdir()): if ( file.is_file() and file.suffix.lower() in IMAGE_EXTENSIONS ): content_relative = file.relative_to( CONTENT_DIR ).as_posix() image_src = image_src_for_page( content_relative, current_output_file ) images.append({ "alt": "", "src": image_src, **image_size_for_content_relative( content_relative ) }) return images def resolve_manual_image_paths( images: list[dict], current_output_file: Path, source_file: Path ) -> list[dict]: resolved_images = [] for image in images: src = image["src"] if is_external_url(src): resolved_src = src size = fallback_image_size() else: content_relative = content_relative_from_src( src, source_file ) resolved_src = image_src_for_page( content_relative, current_output_file, None ) size = image_size_for_content_relative( content_relative ) resolved_images.append({ "alt": image["alt"], "src": resolved_src, **size }) return resolved_images def card_layout(parameter: str) -> str: normalized = slugify_title(parameter or "") return { "einspaltig": "one", "eine_spalte": "one", "zweispaltig": "two", "zwei_spalten": "two", "dreispaltig": "three", "drei_spalten": "three" }.get(normalized, "one") def render_cards(md_text: str, parameter: str = "") -> str: title, cards = split_h2_blocks(md_text) template = env.get_template("cards.html") return template.render( layout=card_layout(parameter), title=render_markdown(title), cards=[ render_markdown(card) for card in cards ] ) def render_two_columns(md_text: str) -> str: title, columns = split_h2_blocks(md_text) left = columns[0] if len(columns) > 0 else "" right = columns[1] if len(columns) > 1 else "" template = env.get_template("two-columns.html") return template.render( title=render_markdown(title), left=render_markdown(left), right=render_markdown(right) ) def render_timeline(md_text: str) -> str: title, items = split_h2_blocks(md_text) template = env.get_template("timeline.html") return template.render( title=render_markdown(title), items=[ render_markdown(item) for item in items ] ) def render_faq(md_text: str) -> str: title, items = split_h2_blocks(md_text) template = env.get_template("faq.html") return template.render( title=render_markdown(title), items=[ render_markdown(item) for item in items ] ) def render_gallery( md_text: str, current_output_file: Path, source_file: Path, parameter: str | None ) -> str: if parameter: images = collect_gallery_images( parameter, current_output_file ) title_text = md_text.strip() else: manual_images = extract_images(md_text) images = resolve_manual_image_paths( manual_images, current_output_file, source_file ) title_text = remove_markdown_images(md_text) template = env.get_template("gallery.html") external_content_enabled = SETTINGS.get( "privacy", {} ).get("external_content", False) has_external_images = any( is_external_url(image["src"]) for image in images ) return template.render( title=render_markdown(title_text), images=images, gallery=SETTINGS.get("gallery", {}), needs_external_consent=has_external_images and not external_content_enabled ) def render_image_text( md_text: str, current_output_file: Path, source_file: Path ) -> str: image, remaining_text = remove_first_image(md_text) if image: image["src"] = image_src_for_page( image["src"], current_output_file, source_file ) template = env.get_template("image-text.html") return template.render( image=image, content=render_markdown(remaining_text) ) def event_parts( md_text: str, current_output_file: Path, source_file: Path ) -> dict: values, remaining_text = key_value_lines(md_text) image, remaining_text = remove_first_image(remaining_text) if image: image["src"] = image_src_for_page( image["src"], current_output_file, source_file ) return { "facts": { key: render_inline_markdown(value) for key, value in values.items() }, "content": render_markdown(remaining_text), "image": image } def render_event( md_text: str, current_output_file: Path, source_file: Path ) -> str: event = event_parts(md_text, current_output_file, source_file) template = env.get_template("event.html") return template.render( values=event["facts"], content=event["content"], image=event["image"] ) def render_events( md_text: str, current_output_file: Path, source_file: Path ) -> str: title, event_blocks = split_h2_blocks(md_text) events = [] for event_block in event_blocks: events.append(event_parts( event_block, current_output_file, source_file )) template = env.get_template("events.html") return template.render( title=render_markdown(title), events=events ) def render_downloads( md_text: str, current_output_file: Path, source_file: Path ) -> str: title_text = remove_markdown_images(md_text) links = [] for link in extract_links(md_text): href = link["href"] if not is_external_url(href): href = image_src_for_page( href, current_output_file, source_file ) links.append({ "label": link["label"], "href": href }) template = env.get_template("downloads.html") return template.render( title=render_markdown(re.sub( r"\[(.*?)\]\((.*?)\)", "", title_text ).strip()), links=links ) def render_people(md_text: str) -> str: title, people = split_h2_blocks(md_text) template = env.get_template("people.html") return template.render( title=render_markdown(title), people=[ render_markdown(person) for person in people ] ) def render_person(md_text: str) -> str: template = env.get_template("person.html") return template.render( content=render_markdown(md_text) ) def render_sponsors( md_text: str, current_output_file: Path, source_file: Path ) -> str: images = resolve_manual_image_paths( extract_images(md_text), current_output_file, source_file ) title_text = remove_markdown_images(md_text) template = env.get_template("sponsors.html") return template.render( title=render_markdown(title_text), sponsors=images ) def youtube_video_id(url: str) -> str | None: parsed = urlparse(url) host = parsed.netloc.lower().replace("www.", "") path_parts = [ part for part in parsed.path.split("/") if part ] if host == "youtu.be" and path_parts: return path_parts[0] if host.endswith("youtube.com"): if parsed.path == "/watch": return parse_qs(parsed.query).get("v", [None])[0] if len(path_parts) >= 2 and path_parts[0] in { "embed", "shorts" }: return path_parts[1] return None def youtube_embed_url(url: str) -> str | None: video_id = youtube_video_id(url) if not video_id: return None return ( "https://www.youtube.com/embed/" f"{video_id}" "?autoplay=1" "&mute=1" "&loop=1" f"&playlist={video_id}" "&controls=0" "&modestbranding=1" "&playsinline=1" ) def hero_media_from_parameter( parameter: str | None, current_output_file: Path, source_file: Path ) -> dict | None: if not parameter: return None parameter = parameter.strip() suffix = Path(urlparse(parameter).path).suffix.lower() if is_external_url(parameter): embed_url = youtube_embed_url(parameter) if embed_url: return { "type": "youtube", "src": embed_url, "external_url": parameter, "style": "" } if suffix in IMAGE_EXTENSIONS: return { "type": "image", "src": parameter, "style": "" } if suffix in VIDEO_EXTENSIONS: return { "type": "video", "src": parameter, "style": "" } if suffix in IMAGE_EXTENSIONS: return { "type": "image", "src": image_src_for_page( parameter, current_output_file, source_file ), "style": "" } if suffix in VIDEO_EXTENSIONS: return { "type": "video", "src": image_src_for_page( parameter, current_output_file, source_file ), "style": "" } return { "type": "color", "src": "", "style": f"background: {parameter};" } def render_hero( md_text: str, current_output_file: Path, source_file: Path, parameter: str | None ) -> str: template = env.get_template("hero.html") return template.render( content=render_markdown(md_text), external_content_enabled=SETTINGS.get( "privacy", {} ).get("external_content", False), hero=hero_media_from_parameter( parameter, current_output_file, source_file ) ) def osm_search_url(address: str) -> str: return ( "https://www.openstreetmap.org/search?query=" f"{quote_plus(address)}" ) def render_location( md_text: str, parameter: str | None ) -> str: address = (parameter or "").strip() content = md_text.strip() if not address: values, remaining_text = key_value_lines(md_text) address = values.get("Adresse", values.get("adresse", "")).strip() content = remaining_text map_url = osm_search_url(address) if address else "" template = env.get_template("location.html") return template.render( address=address, content=render_markdown(content), map_url=map_url, external_content_enabled=SETTINGS.get( "privacy", {} ).get("external_content", False) ) def render_footer( md_text: str, logo: dict | None = None, social_links: list[dict] | None = None ) -> str: title, columns = split_h2_blocks(md_text) columns = [ column for column in columns if not re.match( r"(?im)^##\s*(social|soziales|social media)\s*$", column.strip() ) ] template = env.get_template("footer.html") return template.render( title=render_markdown(title), logo=logo, social_links=social_links or [], columns=[ render_markdown(column) for column in columns ] ) def render_block( block: dict, current_output_file: Path, source_file: Path ) -> str: block_type = block["type"] content = block["content"] parameter = block["parameter"] if block_type == "cards": return render_cards(content, parameter) if block_type == "two-columns": return render_two_columns(content) if block_type == "timeline": return render_timeline(content) if block_type == "faq": return render_faq(content) if block_type == "gallery": return render_gallery( content, current_output_file, source_file, parameter ) if block_type == "image-text": return render_image_text( content, current_output_file, source_file ) if block_type == "event": return render_event( content, current_output_file, source_file ) if block_type == "events": return render_events( content, current_output_file, source_file ) if block_type == "downloads": return render_downloads( content, current_output_file, source_file ) if block_type == "person": return render_person(content) if block_type == "people": return render_people(content) if block_type == "sponsors": return render_sponsors( content, current_output_file, source_file ) if block_type == "location": return render_location( content, parameter ) if block_type == "hero": return render_hero( content, current_output_file, source_file, parameter ) template_name = f"{block_type}.html" if not (TEMPLATE_DIR / template_name).exists(): template_name = "text.html" template = env.get_template(template_name) return template.render( content=render_markdown(content) ) def title_from_file(md_file: Path) -> str: stem = display_name(md_file.stem) if stem == "index": if md_file.parent == CONTENT_DIR: return "Start" return display_name(md_file.parent.name).replace( "-", " " ).title() return stem.replace( "-", " " ).title() def display_name(name: str) -> str: return re.sub(r"^\d+[_-]+", "", name) def output_path_from_file(md_file: Path) -> Path: relative = md_file.relative_to(CONTENT_DIR) relative_parts = [ display_name(part) for part in relative.parts ] relative = Path(*relative_parts) if display_name(md_file.stem) == "index": return OUTPUT_DIR / relative.parent / "index.html" return OUTPUT_DIR / relative.with_suffix(".html") def is_footer_file(md_file: Path) -> bool: relative = md_file.relative_to(CONTENT_DIR) return ( len(relative.parts) >= 2 and relative.parts[0] in {FOOTER_DIR_NAME, LEGACY_FOOTER_DIR_NAME} and md_file.name in {FOOTER_FILE_NAME, LEGACY_FOOTER_FILE_NAME} ) def is_settings_file(md_file: Path) -> bool: return md_file == SETTINGS_MARKDOWN_FILE def static_path_from_output(output_file: Path) -> str: return href_from_output( output_file, OUTPUT_DIR / "static" / "style.css" ) def logo_for_page( settings: dict, current_output_file: Path ) -> dict: logo = settings.get("logo", {}).copy() logo_src = logo.get("src", "").strip().strip("/") logo_folder = logo.get("folder", "logo").strip().strip("/") or "logo" if not logo_src: logo_dir = CONTENT_DIR / MEDIA_DIR_NAME / logo_folder if not logo_dir.exists(): logo_dir = CONTENT_DIR / LEGACY_MEDIA_DIR_NAME / logo_folder logo_files = sorted([ file.name for file in logo_dir.iterdir() if file.is_file() and file.suffix.lower() in IMAGE_EXTENSIONS ]) if logo_dir.exists() else [] if logo_files: logo_src = f"{MEDIA_DIR_NAME}/{logo_folder}/{logo_files[0]}" if logo_src: if "/" not in logo_src: logo_src = f"{MEDIA_DIR_NAME}/{logo_folder}/{logo_src}" logo["url"] = image_src_for_page( logo_src, current_output_file ) else: logo["url"] = "" logo["home_url"] = href_from_output( current_output_file, OUTPUT_DIR / "index.html" ) return logo def social_for_page(settings: dict) -> list[dict]: social = settings.get("social", {}) icon_style = settings.get("icons", {}).get("style", "round") labels = { "instagram": "Instagram", "youtube": "YouTube", "facebook": "Facebook", "x": "X", "twitter": "Twitter", "linkedin": "LinkedIn" } icons = { "instagram": "IG", "youtube": "YT", "facebook": "FB", "x": "X", "twitter": "TW", "linkedin": "IN" } return [ { "key": key, "label": labels.get(key, key.title()), "icon": ( labels.get(key, key.title()) if icon_style == "text" else icons.get(key, key[:2].upper()) ), "url": url } for key, url in social.items() if url ] def slugify_title(title: str) -> str: slug = title.strip().lower() slug = slug.replace("ä", "ae") slug = slug.replace("ö", "oe") slug = slug.replace("ü", "ue") slug = slug.replace("ß", "ss") slug = re.sub( r"[^a-z0-9]+", "-", slug ) slug = slug.strip("-") return slug def build_page_index() -> dict: page_index = {} for md_file in CONTENT_DIR.rglob("*.md"): if is_footer_file(md_file): continue if is_settings_file(md_file): continue key_from_filename = md_file.stem.lower() key_from_title = slugify_title( title_from_file(md_file) ) target = output_path_from_file(md_file) page_index[key_from_filename] = target page_index[key_from_title] = target return page_index def resolve_wiki_links( text: str, current_output_file: Path, page_index: dict ) -> str: pattern = r"\[\[(.*?)\]\]" def replacer(match): label = match.group(1).strip() key = slugify_title(label) if key not in page_index: return label target_output = page_index[key] href = href_from_output( current_output_file, target_output ) return f"[{label}]({href})" return re.sub(pattern, replacer, text) def build_navigation_raw() -> list[dict]: navigation = [] for md_file in sorted(CONTENT_DIR.glob("*.md")): if md_file.name == FOOTER_FILE_NAME: continue if is_settings_file(md_file): continue navigation.append({ "type": "link", "title": title_from_file(md_file), "target": output_path_from_file(md_file) }) for folder in sorted([ p for p in CONTENT_DIR.iterdir() if p.is_dir() ]): if folder.name == FOOTER_DIR_NAME: continue if folder.name in {MEDIA_DIR_NAME, LEGACY_MEDIA_DIR_NAME}: continue children = [] for md_file in sorted(folder.glob("*.md")): children.append({ "title": title_from_file(md_file), "target": output_path_from_file(md_file) }) if children: navigation.append({ "type": "dropdown", "title": display_name(folder.name).replace( "-", " " ).title(), "children": children }) return navigation def navigation_for_page( navigation_raw: list[dict], current_output_file: Path ) -> list[dict]: navigation = [] for item in navigation_raw: if item["type"] == "link": navigation.append({ "type": "link", "title": item["title"], "url": href_from_output( current_output_file, item["target"] ) }) elif item["type"] == "dropdown": children = [] for child in item["children"]: children.append({ "title": child["title"], "url": href_from_output( current_output_file, child["target"] ) }) navigation.append({ "type": "dropdown", "title": item["title"], "children": children }) return navigation def render_footer_for_page( current_output_file: Path, page_index: dict ) -> str: footer_file = CONTENT_DIR / FOOTER_DIR_NAME / FOOTER_FILE_NAME if not footer_file.exists(): footer_file = CONTENT_DIR / FOOTER_DIR_NAME / LEGACY_FOOTER_FILE_NAME if not footer_file.exists(): footer_file = CONTENT_DIR / LEGACY_FOOTER_DIR_NAME / LEGACY_FOOTER_FILE_NAME if not footer_file.exists(): return "" footer_md = footer_file.read_text( encoding="utf-8" ) footer_md = resolve_wiki_links( footer_md, current_output_file, page_index ) return render_footer( footer_md, logo_for_page( SETTINGS, current_output_file ), social_for_page(SETTINGS) ) def build_page( input_file: Path, output_file: Path, navigation_raw: list[dict], page_index: dict ): md_text = input_file.read_text( encoding="utf-8" ) md_text = resolve_wiki_links( md_text, output_file, page_index ) blocks = parse_blocks(md_text) has_gallery = any( block["type"] == "gallery" for block in blocks ) body = "\n".join([ render_block( block, output_file, input_file ) for block in blocks ]) footer = render_footer_for_page( output_file, page_index ) html = env.get_template( "base.html" ).render( body=body, footer=footer, navigation=navigation_for_page( navigation_raw, output_file ), static_path=static_path_from_output(output_file), has_gallery=has_gallery, settings=SETTINGS, logo=logo_for_page( SETTINGS, output_file ) ) output_file.parent.mkdir( parents=True, exist_ok=True ) output_file.write_text( html, encoding="utf-8" ) def copy_static(): target = OUTPUT_DIR / "static" if target.exists(): shutil.rmtree(target) shutil.copytree( STATIC_DIR, target ) def copy_content_media(): source = CONTENT_DIR / MEDIA_DIR_NAME target = OUTPUT_DIR / MEDIA_DIR_NAME if not source.exists(): source = CONTENT_DIR / LEGACY_MEDIA_DIR_NAME if not source.exists(): return if target.exists(): shutil.rmtree(target) shutil.copytree( source, target ) def build_site(format_settings: bool = True): global SETTINGS run_validation(format_settings=format_settings) SETTINGS = load_settings() if OUTPUT_DIR.exists(): shutil.rmtree(OUTPUT_DIR) OUTPUT_DIR.mkdir(exist_ok=True) copy_static() copy_content_media() navigation_raw = build_navigation_raw() page_index = build_page_index() for md_file in CONTENT_DIR.rglob("*.md"): if is_footer_file(md_file): continue if is_settings_file(md_file): continue output_file = output_path_from_file( md_file ) build_page( md_file, output_file, navigation_raw, page_index ) print( f"gebaut: " f"{md_file.relative_to(ROOT_DIR)} " f"→ " f"{output_file.relative_to(ROOT_DIR)}" ) if __name__ == "__main__": build_site()