1023 lines
28 KiB
Python
1023 lines
28 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import json
|
|
import re
|
|
|
|
|
|
ROOT_DIR = Path(__file__).resolve().parent.parent
|
|
CONTENT_DIR = ROOT_DIR / "havelseite"
|
|
LEGACY_CONTENT_DIR = ROOT_DIR / "inhalt"
|
|
OLD_CONTENT_DIR = ROOT_DIR / "content"
|
|
GENERATOR_DIR = ROOT_DIR / "generator"
|
|
SETTINGS_JSON_FILE = GENERATOR_DIR / "settings.json"
|
|
SETTINGS_MARKDOWN_FILE = CONTENT_DIR / "einstellungen.md"
|
|
LEGACY_SETTINGS_MARKDOWN_FILE = LEGACY_CONTENT_DIR / "einstellungen.md"
|
|
OLD_SETTINGS_MARKDOWN_FILE = OLD_CONTENT_DIR / "settings.md"
|
|
THEMES_DIR = GENERATOR_DIR / "themes"
|
|
MEDIA_DIR_NAME = "medien"
|
|
LEGACY_MEDIA_DIR_NAME = "assets"
|
|
|
|
IMAGE_EXTENSIONS = {
|
|
".jpg",
|
|
".jpeg",
|
|
".png",
|
|
".gif",
|
|
".webp",
|
|
".svg"
|
|
}
|
|
|
|
|
|
def load_color_themes() -> dict:
|
|
themes = {}
|
|
|
|
if not THEMES_DIR.exists():
|
|
return themes
|
|
|
|
for file in sorted(THEMES_DIR.glob("*.json")):
|
|
with file.open(encoding="utf-8") as f:
|
|
themes[file.stem] = json.load(f)
|
|
|
|
return themes
|
|
|
|
|
|
COLOR_THEMES = load_color_themes()
|
|
|
|
THEME_ORDER = [
|
|
"havel",
|
|
"wasser",
|
|
"wald",
|
|
"sonnenuntergang",
|
|
"dunkel",
|
|
"kueste",
|
|
"segel",
|
|
"leuchtturm"
|
|
]
|
|
|
|
THEME_LABELS = {
|
|
"dunkel": "Dunkel",
|
|
"havel": "Havel",
|
|
"kueste": "Küste",
|
|
"leuchtturm": "Leuchtturm",
|
|
"segel": "Segel",
|
|
"sonnenuntergang": "Sonnenuntergang",
|
|
"wald": "Wald",
|
|
"wasser": "Wasser"
|
|
}
|
|
|
|
THEME_ALIASES = {
|
|
"ocean": "wasser",
|
|
"forest": "wald",
|
|
"sunset": "sonnenuntergang",
|
|
"dark": "dunkel"
|
|
}
|
|
|
|
|
|
DEFAULT_SETTINGS = {
|
|
"title": "Havelseiten",
|
|
"language": "de",
|
|
"validator_language": "de",
|
|
"theme": "havel",
|
|
"navigation": {
|
|
"height": "medium",
|
|
"position": "sticky",
|
|
"align": "center"
|
|
},
|
|
"mobile_menu": {
|
|
"align": "auto",
|
|
"submenus": "closed"
|
|
},
|
|
"logo": {
|
|
"enabled": True,
|
|
"header": True,
|
|
"footer": False,
|
|
"align": "left",
|
|
"folder": "logo",
|
|
"src": "",
|
|
"alt": "Märkischer Seglerverein Beetzsee",
|
|
"text": "Havelseiten"
|
|
},
|
|
"gallery": {
|
|
"layout": "square",
|
|
"captions": True,
|
|
"rounded": True
|
|
},
|
|
"icons": {
|
|
"style": "round"
|
|
},
|
|
"privacy": {
|
|
"external_content": False
|
|
},
|
|
"typography": {
|
|
"size": "medium"
|
|
},
|
|
"social": {},
|
|
"colors": {}
|
|
}
|
|
|
|
|
|
SETTING_ALIASES = {
|
|
"seite": "site",
|
|
"website": "site",
|
|
"allgemein": "site",
|
|
"sprache": "language",
|
|
"language": "language",
|
|
"pruefung": "language",
|
|
"pruefsprache": "language",
|
|
"prüfung": "language",
|
|
"design": "theme",
|
|
"farbpalette": "theme",
|
|
"theme": "theme",
|
|
"farben": "colors",
|
|
"farbe": "colors",
|
|
"navigation": "navigation",
|
|
"nav": "navigation",
|
|
"mobilmenue": "mobile_menu",
|
|
"mobilmenu": "mobile_menu",
|
|
"mobile_menu": "mobile_menu",
|
|
"burger": "mobile_menu",
|
|
"logo": "logo",
|
|
"logo_datei": "logo_file",
|
|
"logodatei": "logo_file",
|
|
"logo-datei": "logo_file",
|
|
"logo_file": "logo_file",
|
|
"galerie": "gallery",
|
|
"gallery": "gallery",
|
|
"icons": "icons",
|
|
"symbole": "icons",
|
|
"schrift": "typography",
|
|
"schriftart": "typography",
|
|
"typography": "typography",
|
|
"social": "social",
|
|
"soziales": "social",
|
|
"soziale_medien": "social",
|
|
"bilder": "images",
|
|
"images": "images",
|
|
"datenschutz": "privacy",
|
|
"privacy": "privacy",
|
|
"externe_inhalte": "privacy"
|
|
}
|
|
|
|
SIZE_ALIASES = {
|
|
"small": "small",
|
|
"klein": "small",
|
|
"medium": "medium",
|
|
"mittel": "medium",
|
|
"large": "large",
|
|
"gross": "large",
|
|
"groß": "large"
|
|
}
|
|
|
|
POSITION_ALIASES = {
|
|
"sticky": "sticky",
|
|
"bleibt_oben": "sticky",
|
|
"oben_bleiben": "sticky",
|
|
"klebt_oben": "sticky",
|
|
"static": "static",
|
|
"scrollt_mit": "static",
|
|
"normal": "static"
|
|
}
|
|
|
|
ALIGN_ALIASES = {
|
|
"left": "left",
|
|
"links": "left",
|
|
"center": "center",
|
|
"mitte": "center",
|
|
"mittig": "center",
|
|
"right": "right",
|
|
"rechts": "right"
|
|
}
|
|
|
|
MOBILE_MENU_ALIASES = {
|
|
"auto": "auto",
|
|
"automatisch": "auto",
|
|
"left": "left",
|
|
"links": "left",
|
|
"center": "center",
|
|
"mitte": "center",
|
|
"mittig": "center",
|
|
"right": "right",
|
|
"rechts": "right"
|
|
}
|
|
|
|
MOBILE_SUBMENU_ALIASES = {
|
|
"open": "open",
|
|
"offen": "open",
|
|
"aufgeklappt": "open",
|
|
"ausgeklappt": "open",
|
|
"untermenues_offen": "open",
|
|
"untermenus_offen": "open",
|
|
"untermenues_ausgeklappt": "open",
|
|
"untermenus_ausgeklappt": "open",
|
|
"expanded": "open",
|
|
"closed": "closed",
|
|
"geschlossen": "closed",
|
|
"eingeklappt": "closed",
|
|
"einklappbar": "closed",
|
|
"untermenues_einklappen": "closed",
|
|
"untermenus_einklappen": "closed",
|
|
"untermenues_einklappbar": "closed",
|
|
"untermenus_einklappbar": "closed",
|
|
"collapsed": "closed"
|
|
}
|
|
|
|
GALLERY_LAYOUT_ALIASES = {
|
|
"square": "square",
|
|
"quadratisch": "square",
|
|
"wide": "wide",
|
|
"breit": "wide",
|
|
"masonry": "masonry",
|
|
"kacheln": "masonry"
|
|
}
|
|
|
|
ICON_STYLE_ALIASES = {
|
|
"round": "round",
|
|
"rund": "round",
|
|
"simple": "simple",
|
|
"schlicht": "simple",
|
|
"text": "text"
|
|
}
|
|
|
|
FONT_ALIASES = {
|
|
"system": "system",
|
|
"standard": "system",
|
|
"modern": "modern",
|
|
"klassisch": "classic",
|
|
"classic": "classic",
|
|
"freundlich": "friendly",
|
|
"friendly": "friendly"
|
|
}
|
|
|
|
FONT_SIZE_ALIASES = {
|
|
"small": "small",
|
|
"klein": "small",
|
|
"medium": "medium",
|
|
"mittel": "medium",
|
|
"normal": "medium",
|
|
"large": "large",
|
|
"gross": "large",
|
|
"groß": "large"
|
|
}
|
|
|
|
CHOICE_SETTINGS = {
|
|
"language",
|
|
"navigation",
|
|
"logo_file",
|
|
"mobile_menu",
|
|
"icons",
|
|
"typography"
|
|
}
|
|
|
|
OPTIONAL_CHECKBOX_OPTIONS = {
|
|
"untermenues_ausgeklappt",
|
|
"untermenus_ausgeklappt"
|
|
}
|
|
|
|
|
|
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 slug(value: str) -> str:
|
|
value = value.strip().lower()
|
|
value = value.replace("ä", "ae")
|
|
value = value.replace("ö", "oe")
|
|
value = value.replace("ü", "ue")
|
|
value = value.replace("ß", "ss")
|
|
value = re.sub(r"[^a-z0-9]+", "_", value)
|
|
|
|
return value.strip("_")
|
|
|
|
|
|
def normalize_choice(value: str, aliases: dict[str, str]) -> str:
|
|
value_slug = slug(value)
|
|
|
|
return aliases.get(value_slug, value_slug)
|
|
|
|
|
|
def active_content_dir() -> Path:
|
|
if CONTENT_DIR.exists():
|
|
return CONTENT_DIR
|
|
|
|
if LEGACY_CONTENT_DIR.exists():
|
|
return LEGACY_CONTENT_DIR
|
|
|
|
return OLD_CONTENT_DIR
|
|
|
|
|
|
def active_settings_file() -> Path | None:
|
|
if SETTINGS_MARKDOWN_FILE.exists():
|
|
return SETTINGS_MARKDOWN_FILE
|
|
|
|
if LEGACY_SETTINGS_MARKDOWN_FILE.exists():
|
|
return LEGACY_SETTINGS_MARKDOWN_FILE
|
|
|
|
if OLD_SETTINGS_MARKDOWN_FILE.exists():
|
|
return OLD_SETTINGS_MARKDOWN_FILE
|
|
|
|
return None
|
|
|
|
|
|
def parse_bool(value: str) -> bool:
|
|
return slug(value) in {"true", "yes", "ja", "enabled", "aktiv", "an", "anzeigen"}
|
|
|
|
|
|
def selected_options(lines: list[str]) -> list[str]:
|
|
selected = []
|
|
|
|
for line in lines:
|
|
match = re.match(r"^-\s*\[(.*?)\]\s*(.+)$", line.strip())
|
|
|
|
if match and "x" in match.group(1).lower():
|
|
selected.append(match.group(2).strip())
|
|
|
|
return selected
|
|
|
|
|
|
def setting_sections(text: str) -> list[tuple[str, list[str]]]:
|
|
sections = []
|
|
current_name = None
|
|
current_lines = []
|
|
|
|
for line in text.splitlines():
|
|
match = re.match(r"^@(setting|einstellung):(.+)$", line.strip(), re.I)
|
|
|
|
if match:
|
|
if current_name:
|
|
sections.append((current_name, current_lines))
|
|
|
|
current_name = slug(match.group(2))
|
|
current_lines = []
|
|
elif current_name:
|
|
current_lines.append(line)
|
|
|
|
if current_name:
|
|
sections.append((current_name, current_lines))
|
|
|
|
return sections
|
|
|
|
|
|
def selected_by_section(text: str) -> dict[str, list[str]]:
|
|
return {
|
|
SETTING_ALIASES.get(name, name): selected_options(lines)
|
|
for name, lines in setting_sections(text)
|
|
}
|
|
|
|
|
|
def theme_keys() -> list[str]:
|
|
known = [key for key in THEME_ORDER if key in COLOR_THEMES]
|
|
extra = sorted(key for key in COLOR_THEMES if key not in THEME_ORDER)
|
|
|
|
return known + extra
|
|
|
|
|
|
def display_theme_name(key: str) -> str:
|
|
if key in THEME_LABELS:
|
|
return THEME_LABELS[key]
|
|
|
|
return key.replace("_", " ").title()
|
|
|
|
|
|
def normalize_checkbox_lines(text: str) -> str:
|
|
lines = []
|
|
|
|
for line in text.splitlines():
|
|
match = re.match(r"^(\s*-\s*)\[(.*?)\](.*)$", line)
|
|
|
|
if match:
|
|
marker = "x" if "x" in match.group(2).lower() else " "
|
|
lines.append(f"{match.group(1)}[{marker}]{match.group(3)}")
|
|
else:
|
|
lines.append(line.rstrip())
|
|
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
|
|
def refresh_theme_options(text: str) -> str:
|
|
lines = text.splitlines()
|
|
formatted = []
|
|
index = 0
|
|
|
|
while index < len(lines):
|
|
line = lines[index]
|
|
match = re.match(r"^@(setting|einstellung):(.+)$", line.strip(), re.I)
|
|
setting_name = None
|
|
|
|
if match:
|
|
setting_name = SETTING_ALIASES.get(slug(match.group(2)), slug(match.group(2)))
|
|
|
|
if setting_name != "theme":
|
|
formatted.append(line)
|
|
index += 1
|
|
continue
|
|
|
|
formatted.append(line)
|
|
index += 1
|
|
section_lines = []
|
|
|
|
while index < len(lines):
|
|
next_line = lines[index]
|
|
|
|
if re.match(r"^@(setting|einstellung):(.+)$", next_line.strip(), re.I):
|
|
break
|
|
|
|
section_lines.append(next_line)
|
|
index += 1
|
|
|
|
selected_themes = {
|
|
normalize_theme(option)
|
|
for option in selected_options(section_lines)
|
|
}
|
|
|
|
if not selected_themes:
|
|
selected_themes = {DEFAULT_SETTINGS["theme"]}
|
|
|
|
for key in theme_keys():
|
|
marker = "x" if key in selected_themes else " "
|
|
formatted.append(f"- [{marker}] {display_theme_name(key)}")
|
|
|
|
if index < len(lines):
|
|
formatted.append("")
|
|
|
|
return "\n".join(formatted).rstrip() + "\n"
|
|
|
|
|
|
def logo_folder_from_text(text: str) -> str:
|
|
for raw_name, lines in setting_sections(text):
|
|
name = SETTING_ALIASES.get(raw_name, raw_name)
|
|
|
|
if name != "logo":
|
|
continue
|
|
|
|
values = key_values(lines)
|
|
|
|
for key in ["logoordner", "logo_ordner", "ordner"]:
|
|
if key in values:
|
|
return values[key].strip().strip("/") or DEFAULT_SETTINGS["logo"]["folder"]
|
|
|
|
return DEFAULT_SETTINGS["logo"]["folder"]
|
|
|
|
|
|
def logo_files(folder: str) -> list[str]:
|
|
content_dir = active_content_dir()
|
|
logo_dir = content_dir / MEDIA_DIR_NAME / folder
|
|
|
|
if not logo_dir.exists():
|
|
logo_dir = content_dir / LEGACY_MEDIA_DIR_NAME / folder
|
|
|
|
if not logo_dir.exists():
|
|
return []
|
|
|
|
return sorted([
|
|
file.name for file in logo_dir.iterdir()
|
|
if file.is_file() and file.suffix.lower() in IMAGE_EXTENSIONS
|
|
])
|
|
|
|
|
|
def logo_file_lines(files: list[str], selected: list[str]) -> list[str]:
|
|
selected_files = {file for file in selected if file in files}
|
|
|
|
if not selected_files and files:
|
|
selected_files = {files[0]}
|
|
|
|
return [
|
|
f"- [{'x' if file in selected_files else ' '}] {file}"
|
|
for file in files
|
|
]
|
|
|
|
|
|
def refresh_logo_file_options(text: str) -> str:
|
|
files = logo_files(logo_folder_from_text(text))
|
|
|
|
if not files:
|
|
return text
|
|
|
|
lines = text.splitlines()
|
|
formatted = []
|
|
index = 0
|
|
has_logo_file_section = any(
|
|
SETTING_ALIASES.get(raw_name, raw_name) == "logo_file"
|
|
for raw_name, _ in setting_sections(text)
|
|
)
|
|
|
|
while index < len(lines):
|
|
line = lines[index]
|
|
match = re.match(r"^@(setting|einstellung):(.+)$", line.strip(), re.I)
|
|
setting_name = None
|
|
|
|
if match:
|
|
setting_name = SETTING_ALIASES.get(slug(match.group(2)), slug(match.group(2)))
|
|
|
|
if setting_name == "logo_file":
|
|
formatted.append("@einstellung:logo-datei")
|
|
index += 1
|
|
section_lines = []
|
|
|
|
while index < len(lines):
|
|
next_line = lines[index]
|
|
|
|
if re.match(r"^@(setting|einstellung):(.+)$", next_line.strip(), re.I):
|
|
break
|
|
|
|
section_lines.append(next_line)
|
|
index += 1
|
|
|
|
formatted.extend(logo_file_lines(files, selected_options(section_lines)))
|
|
|
|
if index < len(lines) and formatted[-1] != "":
|
|
formatted.append("")
|
|
|
|
continue
|
|
|
|
formatted.append(line)
|
|
index += 1
|
|
|
|
if setting_name == "logo" and not has_logo_file_section:
|
|
while index < len(lines):
|
|
next_line = lines[index]
|
|
|
|
if re.match(r"^@(setting|einstellung):(.+)$", next_line.strip(), re.I):
|
|
break
|
|
|
|
formatted.append(next_line)
|
|
index += 1
|
|
|
|
if formatted[-1] != "":
|
|
formatted.append("")
|
|
formatted.append("@einstellung:logo-datei")
|
|
formatted.extend(logo_file_lines(files, []))
|
|
|
|
if index < len(lines) and formatted[-1] != "":
|
|
formatted.append("")
|
|
|
|
return "\n".join(formatted).rstrip() + "\n"
|
|
|
|
|
|
def checkbox_group_has_choice(lines: list[str]) -> bool:
|
|
return any(
|
|
re.match(r"^\s*-\s*\[x\]", line, re.I)
|
|
for line in lines
|
|
)
|
|
|
|
|
|
def checkbox_group_is_optional(lines: list[str]) -> bool:
|
|
options = [
|
|
slug(match.group(1))
|
|
for line in lines
|
|
if (match := re.match(r"^\s*-\s*\[(?:.*?)\]\s*(.+)$", line))
|
|
]
|
|
|
|
return bool(options) and all(
|
|
option in OPTIONAL_CHECKBOX_OPTIONS
|
|
for option in options
|
|
)
|
|
|
|
|
|
def default_first_option(text: str) -> str:
|
|
lines = text.splitlines()
|
|
formatted = []
|
|
section_name = None
|
|
checkbox_group = []
|
|
|
|
def flush_group():
|
|
nonlocal checkbox_group
|
|
|
|
if not checkbox_group:
|
|
return
|
|
|
|
should_default = (
|
|
section_name in CHOICE_SETTINGS
|
|
and not checkbox_group_has_choice(checkbox_group)
|
|
and not checkbox_group_is_optional(checkbox_group)
|
|
)
|
|
|
|
for offset, group_line in enumerate(checkbox_group):
|
|
if should_default and offset == 0:
|
|
formatted.append(re.sub(r"\[(.*?)\]", "[x]", group_line, count=1))
|
|
else:
|
|
formatted.append(group_line)
|
|
|
|
checkbox_group = []
|
|
|
|
for line in lines:
|
|
section_match = re.match(r"^@(setting|einstellung):(.+)$", line.strip(), re.I)
|
|
checkbox_match = re.match(r"^\s*-\s*\[(.*?)\]", line)
|
|
|
|
if section_match:
|
|
flush_group()
|
|
section_name = SETTING_ALIASES.get(
|
|
slug(section_match.group(2)),
|
|
slug(section_match.group(2))
|
|
)
|
|
formatted.append(line)
|
|
continue
|
|
|
|
if checkbox_match:
|
|
checkbox_group.append(line)
|
|
continue
|
|
|
|
flush_group()
|
|
formatted.append(line)
|
|
|
|
flush_group()
|
|
|
|
return "\n".join(formatted).rstrip() + "\n"
|
|
|
|
|
|
def format_settings_markdown(text: str) -> str:
|
|
text = normalize_checkbox_lines(text)
|
|
text = refresh_theme_options(text)
|
|
text = refresh_logo_file_options(text)
|
|
|
|
return default_first_option(text)
|
|
|
|
|
|
def format_settings_file() -> bool:
|
|
settings_file = active_settings_file()
|
|
|
|
if not settings_file:
|
|
return False
|
|
|
|
original = settings_file.read_text(encoding="utf-8")
|
|
formatted = format_settings_markdown(original)
|
|
|
|
if formatted == original:
|
|
return False
|
|
|
|
settings_file.write_text(formatted, encoding="utf-8")
|
|
return True
|
|
|
|
|
|
def key_values(lines: list[str]) -> dict:
|
|
values = {}
|
|
|
|
for line in lines:
|
|
match = re.match(r"^([A-Za-zÄÖÜäöüß _-]+):\s*(.+)$", line.strip())
|
|
|
|
if match:
|
|
values[slug(match.group(1))] = match.group(2).strip()
|
|
|
|
return values
|
|
|
|
|
|
def normalize_theme(value: str) -> str:
|
|
theme = slug(value)
|
|
|
|
return THEME_ALIASES.get(theme, theme)
|
|
|
|
|
|
def selected_slugs(selected: list[str]) -> set[str]:
|
|
return {slug(value) for value in selected}
|
|
|
|
|
|
def selected_first(selected: list[str], aliases: dict[str, str]) -> str | None:
|
|
for value in selected:
|
|
normalized = normalize_choice(value, aliases)
|
|
|
|
if normalized in aliases.values():
|
|
return normalized
|
|
|
|
return None
|
|
|
|
|
|
def parse_settings_markdown(text: str) -> dict:
|
|
settings = {}
|
|
|
|
for raw_name, lines in setting_sections(text):
|
|
name = SETTING_ALIASES.get(raw_name, raw_name)
|
|
values = key_values(lines)
|
|
selected = selected_options(lines)
|
|
selected_names = selected_slugs(selected)
|
|
|
|
if name == "site":
|
|
if "title" in values:
|
|
settings["title"] = values["title"]
|
|
if "titel" in values:
|
|
settings["title"] = values["titel"]
|
|
if "language" in values:
|
|
settings["language"] = normalize_choice(values["language"], {"deutsch": "de", "english": "en", "englisch": "en"})
|
|
if "sprache" in values:
|
|
settings["language"] = normalize_choice(values["sprache"], {"deutsch": "de", "english": "en", "englisch": "en"})
|
|
if "validator_language" in values:
|
|
settings["validator_language"] = normalize_choice(values["validator_language"], {"deutsch": "de", "english": "en", "englisch": "en"})
|
|
if "pruefsprache" in values:
|
|
settings["validator_language"] = normalize_choice(values["pruefsprache"], {"deutsch": "de", "english": "en", "englisch": "en"})
|
|
|
|
elif name == "language":
|
|
language = selected_first(
|
|
selected,
|
|
{"deutsch": "de", "english": "en", "englisch": "en"}
|
|
)
|
|
|
|
if language:
|
|
settings["language"] = language
|
|
settings["validator_language"] = language
|
|
|
|
elif name == "theme":
|
|
if selected:
|
|
settings["theme"] = normalize_theme(selected[0])
|
|
|
|
elif name == "colors":
|
|
settings["colors"] = values
|
|
|
|
elif name == "navigation":
|
|
navigation = {}
|
|
|
|
height = selected_first(selected, SIZE_ALIASES)
|
|
position = selected_first(selected, POSITION_ALIASES)
|
|
align = selected_first(selected, ALIGN_ALIASES)
|
|
|
|
if height:
|
|
navigation["height"] = height
|
|
if position:
|
|
navigation["position"] = position
|
|
if align:
|
|
navigation["align"] = align
|
|
if "height" in values:
|
|
navigation["height"] = normalize_choice(values["height"], SIZE_ALIASES)
|
|
if "hoehe" in values:
|
|
navigation["height"] = normalize_choice(values["hoehe"], SIZE_ALIASES)
|
|
if "position" in values:
|
|
navigation["position"] = normalize_choice(values["position"], POSITION_ALIASES)
|
|
if "verhalten" in values:
|
|
navigation["position"] = normalize_choice(values["verhalten"], POSITION_ALIASES)
|
|
if "ausrichtung" in values:
|
|
navigation["align"] = normalize_choice(values["ausrichtung"], ALIGN_ALIASES)
|
|
legacy_position = normalize_choice(selected[0], POSITION_ALIASES) if selected else None
|
|
|
|
if legacy_position in {"sticky", "static"} and not position and not height:
|
|
navigation["position"] = legacy_position
|
|
|
|
settings["navigation"] = navigation
|
|
|
|
elif name == "logo":
|
|
logo = {}
|
|
|
|
header_selected = any(
|
|
value in selected_names
|
|
for value in {"in_der_navigation", "navigation", "header", "oben"}
|
|
)
|
|
footer_selected = any(
|
|
value in selected_names
|
|
for value in {"in_der_fusszeile", "im_footer", "footer", "fusszeile", "unten"}
|
|
)
|
|
|
|
logo["enabled"] = header_selected or footer_selected
|
|
logo["header"] = header_selected
|
|
logo["footer"] = footer_selected
|
|
|
|
allowed_aligns = {"left", "right"}
|
|
first_allowed = next(
|
|
(
|
|
normalize_choice(value, ALIGN_ALIASES)
|
|
for value in selected
|
|
if normalize_choice(value, ALIGN_ALIASES) in allowed_aligns
|
|
),
|
|
None
|
|
)
|
|
|
|
if first_allowed:
|
|
logo["align"] = first_allowed
|
|
|
|
for key in ["src", "alt", "text"]:
|
|
if key in values:
|
|
logo[key] = values[key]
|
|
|
|
if "datei" in values:
|
|
logo["src"] = values["datei"]
|
|
if "logoordner" in values:
|
|
logo["folder"] = values["logoordner"]
|
|
if "logo_ordner" in values:
|
|
logo["folder"] = values["logo_ordner"]
|
|
if "ordner" in values:
|
|
logo["folder"] = values["ordner"]
|
|
if "alternativtext" in values:
|
|
logo["alt"] = values["alternativtext"]
|
|
if "ausrichtung" in values:
|
|
logo["align"] = normalize_choice(values["ausrichtung"], ALIGN_ALIASES)
|
|
if "header" in values:
|
|
logo["header"] = parse_bool(values["header"])
|
|
if "navigation" in values:
|
|
logo["header"] = parse_bool(values["navigation"])
|
|
if "footer" in values:
|
|
logo["footer"] = parse_bool(values["footer"])
|
|
if "fusszeile" in values:
|
|
logo["footer"] = parse_bool(values["fusszeile"])
|
|
|
|
settings["logo"] = logo
|
|
|
|
elif name == "logo_file":
|
|
if selected:
|
|
settings.setdefault("logo", {})
|
|
settings["logo"]["src"] = selected[0]
|
|
|
|
elif name == "mobile_menu":
|
|
mobile_menu = {}
|
|
align = selected_first(selected, MOBILE_MENU_ALIASES)
|
|
|
|
if align:
|
|
mobile_menu["align"] = align
|
|
mobile_menu["submenus"] = "open" if any(
|
|
slug(value) in OPTIONAL_CHECKBOX_OPTIONS
|
|
for value in selected
|
|
) else "closed"
|
|
if "ausrichtung" in values:
|
|
mobile_menu["align"] = normalize_choice(
|
|
values["ausrichtung"],
|
|
MOBILE_MENU_ALIASES
|
|
)
|
|
if "untermenues" in values:
|
|
mobile_menu["submenus"] = normalize_choice(
|
|
values["untermenues"],
|
|
MOBILE_SUBMENU_ALIASES
|
|
)
|
|
if "untermenus" in values:
|
|
mobile_menu["submenus"] = normalize_choice(
|
|
values["untermenus"],
|
|
MOBILE_SUBMENU_ALIASES
|
|
)
|
|
if "submenus" in values:
|
|
mobile_menu["submenus"] = normalize_choice(
|
|
values["submenus"],
|
|
MOBILE_SUBMENU_ALIASES
|
|
)
|
|
|
|
settings["mobile_menu"] = mobile_menu
|
|
|
|
elif name == "gallery":
|
|
gallery = {}
|
|
|
|
if selected:
|
|
gallery["layout"] = normalize_choice(selected[0], GALLERY_LAYOUT_ALIASES)
|
|
if "layout" in values:
|
|
gallery["layout"] = normalize_choice(values["layout"], GALLERY_LAYOUT_ALIASES)
|
|
if "darstellung" in values:
|
|
gallery["layout"] = normalize_choice(values["darstellung"], GALLERY_LAYOUT_ALIASES)
|
|
if "bildunterschriften" in values:
|
|
gallery["captions"] = parse_bool(values["bildunterschriften"])
|
|
|
|
settings["gallery"] = gallery
|
|
|
|
elif name == "images":
|
|
gallery = {}
|
|
|
|
if "bildunterschriften" in selected_names:
|
|
gallery["captions"] = True
|
|
if "keine_bildunterschriften" in selected_names:
|
|
gallery["captions"] = False
|
|
if "runde_ecken" in selected_names:
|
|
gallery["rounded"] = True
|
|
if "eckige_bilder" in selected_names or "keine_runden_ecken" in selected_names:
|
|
gallery["rounded"] = False
|
|
|
|
if "bildunterschriften" in values:
|
|
gallery["captions"] = parse_bool(values["bildunterschriften"])
|
|
if "runde_ecken" in values:
|
|
gallery["rounded"] = parse_bool(values["runde_ecken"])
|
|
|
|
settings["gallery"] = gallery
|
|
|
|
elif name == "icons":
|
|
icons = {}
|
|
|
|
if selected:
|
|
icons["style"] = normalize_choice(selected[0], ICON_STYLE_ALIASES)
|
|
if "stil" in values:
|
|
icons["style"] = normalize_choice(values["stil"], ICON_STYLE_ALIASES)
|
|
if "style" in values:
|
|
icons["style"] = normalize_choice(values["style"], ICON_STYLE_ALIASES)
|
|
|
|
settings["icons"] = icons
|
|
|
|
elif name == "privacy":
|
|
privacy = {}
|
|
|
|
if "externe_inhalte_laden" in selected_names:
|
|
privacy["external_content"] = True
|
|
if "externe_inhalte_nicht_laden" in selected_names:
|
|
privacy["external_content"] = False
|
|
if "external_content" in values:
|
|
privacy["external_content"] = parse_bool(values["external_content"])
|
|
if "externe_inhalte" in values:
|
|
privacy["external_content"] = parse_bool(values["externe_inhalte"])
|
|
|
|
settings["privacy"] = privacy
|
|
|
|
elif name == "typography":
|
|
typography = {}
|
|
size = selected_first(selected, FONT_SIZE_ALIASES)
|
|
|
|
if size:
|
|
typography["size"] = size
|
|
elif selected:
|
|
typography["font"] = normalize_choice(selected[0], FONT_ALIASES)
|
|
|
|
if "groesse" in values:
|
|
typography["size"] = normalize_choice(values["groesse"], FONT_SIZE_ALIASES)
|
|
if "größe" in values:
|
|
typography["size"] = normalize_choice(values["größe"], FONT_SIZE_ALIASES)
|
|
if "size" in values:
|
|
typography["size"] = normalize_choice(values["size"], FONT_SIZE_ALIASES)
|
|
if "schriftart" in values:
|
|
typography["font"] = normalize_choice(values["schriftart"], FONT_ALIASES)
|
|
if "font" in values:
|
|
typography["font"] = normalize_choice(values["font"], FONT_ALIASES)
|
|
|
|
settings["typography"] = typography
|
|
|
|
elif name == "social":
|
|
settings["social"] = values
|
|
|
|
return settings
|
|
|
|
|
|
def load_json_settings() -> dict:
|
|
if not SETTINGS_JSON_FILE.exists():
|
|
return {}
|
|
|
|
with SETTINGS_JSON_FILE.open(encoding="utf-8") as f:
|
|
return json.load(f)
|
|
|
|
|
|
def load_markdown_settings() -> dict:
|
|
settings_file = active_settings_file()
|
|
|
|
if not settings_file:
|
|
return {}
|
|
|
|
return parse_settings_markdown(
|
|
settings_file.read_text(encoding="utf-8")
|
|
)
|
|
|
|
|
|
def apply_theme(settings: dict) -> dict:
|
|
theme = normalize_theme(settings.get("theme", "havel"))
|
|
fallback = next(iter(COLOR_THEMES.values()), {})
|
|
theme_colors = COLOR_THEMES.get(theme, fallback)
|
|
custom_colors = settings.get("colors", {})
|
|
|
|
settings["theme"] = theme
|
|
settings["colors"] = {
|
|
**theme_colors,
|
|
**custom_colors
|
|
}
|
|
|
|
return settings
|
|
|
|
|
|
def apply_typography(settings: dict) -> dict:
|
|
font = settings.get("typography", {}).get("font", "system")
|
|
size = settings.get("typography", {}).get("size", "medium")
|
|
stacks = {
|
|
"system": {
|
|
"body": "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
"heading": "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
|
|
},
|
|
"modern": {
|
|
"body": "'Avenir Next', Avenir, 'Segoe UI', system-ui, sans-serif",
|
|
"heading": "'Avenir Next', Avenir, 'Segoe UI', system-ui, sans-serif"
|
|
},
|
|
"classic": {
|
|
"body": "Georgia, 'Times New Roman', serif",
|
|
"heading": "Georgia, 'Times New Roman', serif"
|
|
},
|
|
"friendly": {
|
|
"body": "'Trebuchet MS', 'Arial Rounded MT Bold', Verdana, system-ui, sans-serif",
|
|
"heading": "'Trebuchet MS', 'Arial Rounded MT Bold', Verdana, system-ui, sans-serif"
|
|
}
|
|
}
|
|
|
|
settings["typography"] = {
|
|
**settings.get("typography", {}),
|
|
"size": size,
|
|
**stacks.get(font, stacks["system"])
|
|
}
|
|
|
|
return settings
|
|
|
|
|
|
def load_settings() -> dict:
|
|
if active_settings_file():
|
|
settings = merge_settings(
|
|
DEFAULT_SETTINGS,
|
|
load_markdown_settings()
|
|
)
|
|
else:
|
|
settings = merge_settings(
|
|
DEFAULT_SETTINGS,
|
|
load_json_settings()
|
|
)
|
|
|
|
return apply_typography(apply_theme(settings))
|