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
|
python3 scripts/learn_theme.py --file <path> # analyse a saved HTML file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
import colorsys
|
import colorsys
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import yaml
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -157,6 +160,9 @@ _BROWSER_UA = (
|
||||||
"Chrome/124.0.0.0 Safari/537.36"
|
"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:
|
def _attach_title(soup, content) -> None:
|
||||||
"""Find the article title in *soup* and stash it on *content*."""
|
"""Find the article title in *soup* and stash it on *content*."""
|
||||||
|
|
@ -440,6 +446,131 @@ def analyze_styles(grouped: dict) -> dict:
|
||||||
return result
|
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
|
# CLI entry point / smoke test
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue