feat: single-source theme discovery replaces three hardcoded whitelists

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-22 17:14:44 +08:00
parent cd109cf873
commit 204c5039ab
4 changed files with 38 additions and 4 deletions
+18 -1
View File
@@ -4,12 +4,29 @@ import json
import re import re
import socket import socket
import uuid import uuid
from pathlib import Path
from urllib.parse import urlparse, unquote from urllib.parse import urlparse, unquote
import bleach import bleach
from . import db 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') 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_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']} ALLOWED_ATTRS = {'a':['href','title','target','rel'], 'img':['src','alt','title','loading','width','height'], 'td':['colspan','rowspan'], 'th':['colspan','rowspan']}
+1 -2
View File
@@ -13,9 +13,8 @@ from pathlib import Path
from urllib.parse import quote from urllib.parse import quote
from jinja2 import Environment, FileSystemLoader, select_autoescape from jinja2 import Environment, FileSystemLoader, select_autoescape
from markupsafe import Markup 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 ROOT=Path(__file__).resolve().parent.parent
LOADER=Environment(loader=FileSystemLoader(str(ROOT/'themes')),autoescape=select_autoescape(['html','xml'])) LOADER=Environment(loader=FileSystemLoader(str(ROOT/'themes')),autoescape=select_autoescape(['html','xml']))
+1 -1
View File
@@ -8,10 +8,10 @@ from __future__ import annotations
import io, json, re, zipfile, shutil import io, json, re, zipfile, shutil
from pathlib import Path from pathlib import Path
from . import db from . import db
from .core import THEMES as BUILTIN
BASE=Path(__file__).resolve().parent.parent/'themes' BASE=Path(__file__).resolve().parent.parent/'themes'
THEME_ROOT=db.DATA/'theme_packages' THEME_ROOT=db.DATA/'theme_packages'
BUILTIN=('automotive','industrial','corporate','minimal')
THEME_ID=re.compile(r'^[a-z][a-z0-9-]{2,39}$') THEME_ID=re.compile(r'^[a-z][a-z0-9-]{2,39}$')
VERSION=re.compile(r'^\d+\.\d+\.\d+$') VERSION=re.compile(r'^\d+\.\d+\.\d+$')
+18
View File
@@ -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)