Files
havelseiten/generator/settings_config.py
2026-05-16 14:16:54 +02:00

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))