From 204c5039ab5561fddbbd3109c3a2ad387827235f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Thu, 22 Oct 2026 17:14:44 +0800 Subject: [PATCH] feat: single-source theme discovery replaces three hardcoded whitelists Co-Authored-By: Claude Code --- app/core.py | 19 ++++++++++++++++++- app/theme_pro.py | 3 +-- app/theme_store.py | 2 +- tests/test_theme_registry.py | 18 ++++++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 tests/test_theme_registry.py diff --git a/app/core.py b/app/core.py index a36d7c2..93107de 100644 --- a/app/core.py +++ b/app/core.py @@ -4,12 +4,29 @@ import json import re import socket import uuid +from pathlib import Path from urllib.parse import urlparse, unquote import bleach from . import db +ROOT = Path(__file__).resolve().parent.parent + +def discover_themes(): + """Discover built-in themes from themes/*/manifest.json (single source of truth).""" + found = [] + for entry in sorted((ROOT / 'themes').iterdir()): + if not entry.is_dir() or entry.name.startswith('_'): + continue + try: + manifest = json.loads((entry / 'manifest.json').read_text(encoding='utf-8')) + except (OSError, ValueError): + continue + if isinstance(manifest, dict) and manifest.get('id') == entry.name: + found.append(entry.name) + return tuple(found) + TYPES = ('page','product','post','service','case','brand','vehicle','other') -THEMES = ('automotive','industrial','corporate','minimal') +THEMES = discover_themes() ALLOWED_TAGS = ['p','br','b','i','u','em','strong','h1','h2','h3','h4','h5','h6','ul','ol','li','blockquote','pre','code','a','img','figure','figcaption','table','thead','tbody','tr','th','td','hr','div','span'] ALLOWED_ATTRS = {'a':['href','title','target','rel'], 'img':['src','alt','title','loading','width','height'], 'td':['colspan','rowspan'], 'th':['colspan','rowspan']} diff --git a/app/theme_pro.py b/app/theme_pro.py index 281868f..af711f1 100644 --- a/app/theme_pro.py +++ b/app/theme_pro.py @@ -13,9 +13,8 @@ from pathlib import Path from urllib.parse import quote from jinja2 import Environment, FileSystemLoader, select_autoescape from markupsafe import Markup -from .core import sanitize, route_path +from .core import sanitize, route_path, THEMES -THEMES=('automotive','industrial','corporate','minimal') ROOT=Path(__file__).resolve().parent.parent LOADER=Environment(loader=FileSystemLoader(str(ROOT/'themes')),autoescape=select_autoescape(['html','xml'])) diff --git a/app/theme_store.py b/app/theme_store.py index 0863fcf..6536327 100644 --- a/app/theme_store.py +++ b/app/theme_store.py @@ -8,10 +8,10 @@ from __future__ import annotations import io, json, re, zipfile, shutil from pathlib import Path from . import db +from .core import THEMES as BUILTIN BASE=Path(__file__).resolve().parent.parent/'themes' THEME_ROOT=db.DATA/'theme_packages' -BUILTIN=('automotive','industrial','corporate','minimal') THEME_ID=re.compile(r'^[a-z][a-z0-9-]{2,39}$') VERSION=re.compile(r'^\d+\.\d+\.\d+$') diff --git a/tests/test_theme_registry.py b/tests/test_theme_registry.py new file mode 100644 index 0000000..6070702 --- /dev/null +++ b/tests/test_theme_registry.py @@ -0,0 +1,18 @@ +"""Theme discovery must be a single source of truth.""" +import json +from pathlib import Path +from app import core, theme_pro, theme_store + +ROOT = Path(core.__file__).resolve().parent.parent + +def test_discover_returns_builtin_themes_with_manifests(): + ids = core.discover_themes() + for tid in ('automotive', 'industrial', 'corporate', 'minimal'): + assert tid in ids + for tid in ids: + manifest = json.loads((ROOT / 'themes' / tid / 'manifest.json').read_text(encoding='utf-8')) + assert manifest.get('id') == tid + +def test_three_whitelists_agree(): + assert set(core.THEMES) == set(theme_pro.THEMES) == set(theme_store.BUILTIN) + assert isinstance(core.THEMES, tuple)