feat(learn-theme): add theme YAML generation from analyzed styles

Add `generate_theme_yaml()` that builds a complete theme YAML by loading
the professional-clean template CSS, substituting extracted colors and
typography, and deriving a dark-mode palette via `derive_darkmode()`.
Adds `import yaml`, `import argparse`, `from pathlib import Path`, and
module-level constants `TEMPLATE_THEME` / `THEMES_DIR`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangzhuc 2026-04-01 11:45:16 +08:00
parent 77e76077d8
commit 1cd9b4409f

View file

@ -5,12 +5,15 @@ Usage:
python3 scripts/learn_theme.py --file <path> # analyse a saved HTML file
"""
import argparse
import colorsys
import re
import sys
from collections import Counter
from pathlib import Path
import requests
import yaml
from bs4 import BeautifulSoup
# ---------------------------------------------------------------------------
@ -157,6 +160,9 @@ _BROWSER_UA = (
"Chrome/124.0.0.0 Safari/537.36"
)
TEMPLATE_THEME = "professional-clean"
THEMES_DIR = Path(__file__).resolve().parent.parent / "toolkit" / "themes"
def _attach_title(soup, content) -> None:
"""Find the article title in *soup* and stash it on *content*."""
@ -440,6 +446,131 @@ def analyze_styles(grouped: dict) -> dict:
return result
# ---------------------------------------------------------------------------
# 4. Theme YAML generation
# ---------------------------------------------------------------------------
def _load_template_css() -> str:
"""Load the base_css from the professional-clean template theme."""
template_path = THEMES_DIR / f"{TEMPLATE_THEME}.yaml"
with open(template_path, encoding="utf-8") as fh:
data = yaml.safe_load(fh)
return data["base_css"]
def generate_theme_yaml(name: str, title: str, analyzed: dict) -> str:
"""Generate a complete theme YAML string from analyzed style properties.
Parameters
----------
name: Theme identifier (used as filename and --theme reference).
title: Article title (used in description).
analyzed: Dict returned by :func:`analyze_styles`.
"""
css = _load_template_css()
# Build colors dict
colors = {
"primary": analyzed.get("primary", "#2563eb"),
"secondary": analyzed.get("secondary", "#3b82f6"),
"text": analyzed.get("text", "#333333"),
"text_light": analyzed.get("text_light", "#666666"),
"background": analyzed.get("background", "#ffffff"),
"code_bg": analyzed.get("code_bg", "#1e293b"),
"code_color": analyzed.get("code_color", "#e2e8f0"),
"quote_border": analyzed.get("quote_border", "#2563eb"),
"quote_bg": analyzed.get("quote_bg", "#eff6ff"),
"border_radius": analyzed.get("border_radius", "8px"),
}
# Replace template colors in CSS
css = css.replace("#2563eb", colors["primary"])
css = css.replace("#3b82f6", colors["secondary"])
css = css.replace("#333333", colors["text"])
css = css.replace("#666666", colors["text_light"])
css = css.replace("#1e293b", colors["code_bg"])
css = css.replace("#e2e8f0", colors["code_color"])
css = css.replace("#eff6ff", colors["quote_bg"])
# Replace typography via regex
font_size = analyzed.get("font_size", "16px")
line_height = analyzed.get("line_height", "1.8")
font_family = analyzed.get("font_family",
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, '
'"Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", '
'"Microsoft YaHei", sans-serif')
p_margin = analyzed.get("p_margin", "12px 0")
border_radius = colors["border_radius"]
# body font-size
css = re.sub(
r"(body\s*\{[^}]*font-size:\s*)[\d.]+px",
lambda m: m.group(1) + font_size,
css,
flags=re.DOTALL,
)
# body line-height
css = re.sub(
r"(body\s*\{[^}]*line-height:\s*)[\d.]+",
lambda m: m.group(1) + line_height,
css,
flags=re.DOTALL,
)
# body font-family (replace the quoted chain)
css = re.sub(
r'(body\s*\{[^}]*font-family:\s*)[^;]+;',
lambda m: m.group(1) + font_family + ";",
css,
flags=re.DOTALL,
)
# p line-height
css = re.sub(
r'(p\s*\{[^}]*line-height:\s*)[\d.]+',
lambda m: m.group(1) + line_height,
css,
flags=re.DOTALL,
)
# p margin
css = re.sub(
r'(p\s*\{[^}]*margin:\s*)[^;]+;',
lambda m: m.group(1) + p_margin + ";",
css,
flags=re.DOTALL,
)
# li line-height (match p line-height)
css = re.sub(
r'(li\s*\{[^}]*line-height:\s*)[\d.]+',
lambda m: m.group(1) + line_height,
css,
flags=re.DOTALL,
)
# border-radius: 8px and border-radius: 4px
css = css.replace("border-radius: 8px", f"border-radius: {border_radius}")
css = css.replace("border-radius: 4px", f"border-radius: {border_radius}")
# Derive dark mode
darkmode = derive_darkmode(colors)
# Description
if title:
description = f"从「{title}」学习的排版主题"
else:
description = f"Learned theme: {name}"
# Build theme data
colors_with_dark = dict(colors)
colors_with_dark["darkmode"] = darkmode
theme_data = {
"name": name,
"description": description,
"colors": colors_with_dark,
"base_css": css,
}
return yaml.dump(theme_data, allow_unicode=True, default_flow_style=False, sort_keys=False)
# ---------------------------------------------------------------------------
# CLI entry point / smoke test
# ---------------------------------------------------------------------------