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:
parent
77e76077d8
commit
1cd9b4409f
1 changed files with 131 additions and 0 deletions
|
|
@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue