generate-menu.py
tools/generate-menu.py
#!/usr/bin/env python3
"""
Statisches Menü-HTML generieren für SSI-Include.
Gleiche Datei-Walk-Logik wie generate-sitemap.py.
"""
import os
import locale
from html import escape
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUTPUT_FILE = os.path.join(ROOT_DIR, 'includes', 'something-in-the-body.html')
EXCLUDE_DIRS = {
'.git', '.ssh', '.vscode', '00_Archiv', '__pycache__', 'includes',
'fonts', # top-level /fonts and nested .../fonts/...
'!!DANGER!!-un0nefinity-fonts-!!DANGER!!', # belt-and-suspenders for the font pack dir name
'node_modules', '.superpowers', '.gitnexus', '.kilocode',
'.auth', '.php_tmp', 'test-results',
}
EXCLUDE_FILES = {'meta.json'}
# Gleiche Priority-Items wie in meta.js
PRIORITY_ITEMS = [
{'type': 'heading', 'text': 'legal stuff first:'},
{'type': 'file', 'path': 'impressum-und-datenschutz.html', 'name': 'impressum-und-datenschutz'},
{'type': 'heading', 'text': 'now the party:'},
{'type': 'file', 'path': 'README.html', 'name': 'README'},
]
EXCLUDE_FROM_LIST = {'impressum-und-datenschutz.html', 'README.html'}
def should_include(name):
if name.startswith('.'):
return False
if name.startswith('_'):
return False
if name in EXCLUDE_FILES:
return False
return True
def collect_structure():
"""Sammelt Dateien/Ordner in hierarchische Struktur."""
structure = {'folders': {}, 'files': []}
for dirpath, dirnames, filenames in os.walk(ROOT_DIR):
dirnames[:] = sorted(
[d for d in dirnames if not d.startswith('.') and d not in EXCLUDE_DIRS],
key=lambda x: x.lower()
)
rel_dir = os.path.relpath(dirpath, ROOT_DIR)
if rel_dir == '.':
rel_dir = ''
for filename in sorted(filenames, key=lambda x: x.lower()):
if not should_include(filename):
continue
rel_path = os.path.join(rel_dir, filename).replace(os.sep, '/') if rel_dir else filename
parts = rel_path.split('/')
if len(parts) == 1:
# Root-Datei
if filename not in EXCLUDE_FROM_LIST:
name = filename
dot = name.rfind('.')
display = name[:dot] if dot > 0 else name
structure['files'].append({'name': filename, 'path': rel_path, 'display': display})
else:
# In Ordner
current = structure
for folder in parts[:-1]:
if folder not in current['folders']:
current['folders'][folder] = {'folders': {}, 'files': []}
current = current['folders'][folder]
name = parts[-1]
dot = name.rfind('.')
display = name[:dot] if dot > 0 else name
current['files'].append({'name': filename, 'path': rel_path, 'display': display})
return structure
def render_file(f):
href = '/' + f['path']
return f'<li><a href="{escape(href)}">{escape(f["name"])}</a></li>'
def render_folder(name, data):
parts = [f'<li><details><summary>{escape(name)}</summary><ul class="folder-contents">']
for subfolder_name in sorted(data['folders'].keys(), key=lambda x: x.lower()):
parts.append(render_folder(subfolder_name, data['folders'][subfolder_name]))
for f in sorted(data['files'], key=lambda x: x['name'].lower()):
parts.append(render_file(f))
parts.append('</ul></details></li>')
return ''.join(parts)
def generate():
structure = collect_structure()
lines = []
lines.append('<nav id="meta-nav" aria-label="Hauptnavigation">')
lines.append('<a href="/index.html" class="back-button" aria-label="Startseite">\u22C5</a>')
lines.append('<div class="menu">')
lines.append('<input type="checkbox" id="menu-toggle" class="menu-toggle-input">')
lines.append('<label for="menu-toggle" class="menu-backdrop" aria-hidden="true"></label>')
lines.append('<label for="menu-toggle" class="menu-button" aria-label="Men\u00fc"><span class="menu-closed-text">\u2261</span><span class="menu-open-text">0 \u2261 1 \u2261 \u221E</span></label>')
lines.append('<div class="menu-content-wrapper">')
lines.append('<div class="menu-search"><svg class="meta-loupe" viewBox="-79 -498 946 946" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false"><path class="meta-loupe-lens" d="M261 5Q198 5 148.5 -23.0Q99 -51 70.5 -99.0Q42 -147 42 -209Q42 -270 70.5 -317.5Q99 -365 148.0 -392.0Q197 -419 260 -419Q322 -419 371.5 -391.5Q421 -364 449.5 -316.5Q478 -269 478 -206Q478 -146 449.5 -97.5Q421 -49 372.5 -22.0Q324 5 261 5ZM261 -50Q304 -50 338.0 -70.0Q372 -90 391.0 -125.5Q410 -161 410 -206Q410 -251 391.0 -287.0Q372 -323 338.0 -344.0Q304 -365 260 -365Q217 -365 182.5 -345.0Q148 -325 128.0 -290.0Q108 -255 108 -209Q108 -164 128.0 -127.5Q148 -91 183.0 -70.5Q218 -50 261 -50Z"/><g class="meta-loupe-handle"><path d="M196 -414H243V0H175V-394L201 -360L59 -278L34 -322Z"/></g></svg><input type="search" placeholder="activate js or use browser search" disabled aria-label="Suche ben\u00f6tigt JavaScript"></div>')
lines.append('<ul id="file-list">')
# Priority items
for item in PRIORITY_ITEMS:
if item['type'] == 'heading':
lines.append(f'<li><span class="menu-heading">{escape(item["text"])}</span></li>')
elif item['type'] == 'file':
href = '/' + item['path']
lines.append(f'<li><a href="{escape(href)}">{escape(item["name"])}</a></li>')
# Ordner
for folder_name in sorted(structure['folders'].keys(), key=lambda x: x.lower()):
lines.append(render_folder(folder_name, structure['folders'][folder_name]))
# Root-Dateien
for f in sorted(structure['files'], key=lambda x: x['name'].lower()):
lines.append(render_file(f))
lines.append('</ul>')
lines.append('</div>')
lines.append('</div>')
lines.append('</nav>')
lines.append('\n\n<div class="meta-dialog-backdrop" hidden>')
lines.append('\n <div class="meta-dialog" role="dialog" aria-modal="true" aria-labelledby="meta-dialog-title" aria-describedby="meta-dialog-message">')
lines.append('\n <div class="meta-dialog-title" id="meta-dialog-title"></div>')
lines.append('\n <div class="meta-dialog-message" id="meta-dialog-message"></div>')
lines.append('\n <div class="meta-dialog-actions">')
lines.append('\n <button type="button" class="meta-dialog-secondary" data-dialog-action="dismiss"></button>')
lines.append('\n <button type="button" class="meta-dialog-secondary" data-dialog-action="cancel"></button>')
lines.append('\n <button type="button" class="meta-dialog-confirm" data-dialog-action="confirm"></button>')
lines.append('\n </div>')
lines.append('\n </div>')
lines.append('\n</div>')
os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
with open(OUTPUT_FILE, 'w', encoding='utf-8') as out:
out.write(''.join(lines) + '\n')
total = sum(1 for line in lines if '<li><a ' in line)
print(f'menu.html generiert — {total} Einträge')
if __name__ == '__main__':
generate()