feat: single-source theme discovery replaces three hardcoded whitelists
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+18
-1
@@ -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
@@ -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
@@ -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+$')
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user