From d6900fe85db8454b07b3329085c047b7c3cd4c0d Mon Sep 17 00:00:00 2001 From: wangzhuc Date: Wed, 1 Apr 2026 11:45:36 +0800 Subject: [PATCH] feat(learn-theme): add CLI with argparse and terminal report Replace the smoke-test main() with a proper argparse CLI that accepts a URL and --name, validates the name, fetches + extracts + analyzes the article, calls generate_theme_yaml(), and writes the YAML to toolkit/themes/. Prints a human-readable theme report with color values and typography. Adds `learn-theme` subcommand to toolkit/cli.py (delegates to subprocess call of scripts/learn_theme.py). Co-Authored-By: Claude Sonnet 4.6 --- scripts/learn_theme.py | 70 +++++++++++++++++++++++++++++------------- toolkit/cli.py | 16 ++++++++++ 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/scripts/learn_theme.py b/scripts/learn_theme.py index 3a1df7d..e0ef85d 100644 --- a/scripts/learn_theme.py +++ b/scripts/learn_theme.py @@ -588,33 +588,59 @@ def _load_from_file(path: str): def main(): - args = sys.argv[1:] - if not args: - print(__doc__) - sys.exit(0) + parser = argparse.ArgumentParser( + description="Learn a WeChat formatting theme from an article URL.", + ) + parser.add_argument("url", help="WeChat article URL (https://mp.weixin.qq.com/s/...)") + parser.add_argument("--name", required=True, help="Theme name (used as filename and reference)") + parser.add_argument("--output-dir", default=None, help="Output directory (default: toolkit/themes/)") + args = parser.parse_args() - if args[0] == "--file" and len(args) >= 2: - content = _load_from_file(args[1]) - else: - content = fetch_article(args[0]) + # Validate name: only letters, digits, hyphens, underscores + if not re.match(r"^[a-zA-Z0-9_-]+$", args.name): + print("Error: --name must contain only letters, digits, hyphens, and underscores.", file=sys.stderr) + raise SystemExit(1) - print(f"Title: {content._wewrite_title}") + output_dir = Path(args.output_dir) if args.output_dir else THEMES_DIR + output_path = output_dir / f"{args.name}.yaml" + + if output_path.exists(): + print(f"Warning: {output_path} already exists, will be overwritten.", file=sys.stderr) + + # Fetch + print("Fetching article...") + content = fetch_article(args.url) + title = getattr(content, "_wewrite_title", "") + if title: + print(f"Title: {title}") + + # Extract grouped = extract_styles(content) - print("Elements with styles:") - for tag, styles in grouped.items(): - if styles: - print(f" <{tag}>: {len(styles)} elements") + styled_count = sum(len(v) for v in grouped.values()) + print(f"Extracted {styled_count} styled elements.") - theme = analyze_styles(grouped) - print("\nInferred theme:") - for key, val in theme.items(): - print(f" {key}: {val}") + # Analyze + analyzed = analyze_styles(grouped) - # Dark mode - dm = derive_darkmode(theme) - print("\nDerived dark mode:") - for key, val in dm.items(): - print(f" {key}: {val}") + # Generate & write + theme_yaml = generate_theme_yaml(args.name, title, analyzed) + output_dir.mkdir(parents=True, exist_ok=True) + output_path.write_text(theme_yaml, encoding="utf-8") + + # Report + print() + print(f"Learned theme from: {title or args.url}") + print(f" text: {analyzed['text']}") + print(f" text_light: {analyzed['text_light']}") + print(f" primary: {analyzed['primary']}") + print(f" secondary: {analyzed['secondary']}") + print(f" background: {analyzed['background']}") + print(f" font: {analyzed['font_family'][:50]}") + print(f" size: {analyzed['font_size']} / line-height {analyzed['line_height']} / spacing {analyzed['letter_spacing']}") + print() + print(f"Theme saved → {output_path}") + print(f"Use it: python3 toolkit/cli.py preview article.md --theme {args.name}") + print(f"Or set: theme: {args.name} in style.yaml") if __name__ == "__main__": diff --git a/toolkit/cli.py b/toolkit/cli.py index 86f6301..f8f5742 100644 --- a/toolkit/cli.py +++ b/toolkit/cli.py @@ -232,6 +232,15 @@ def cmd_gallery(args): webbrowser.open(f"file://{Path(output).absolute()}") +def cmd_learn_theme(args): + """Learn a theme from a WeChat article URL.""" + import subprocess + script = Path(__file__).parent.parent / "scripts" / "learn_theme.py" + cmd = [sys.executable, str(script), args.url, "--name", args.name] + result = subprocess.run(cmd) + sys.exit(result.returncode) + + def _gallery_sample_markdown(): return """# 示例文章标题 @@ -397,6 +406,11 @@ def main(): p_gallery.add_argument("-o", "--output", help="Output HTML file path") p_gallery.add_argument("--no-open", action="store_true", help="Don't open browser") + # learn-theme + p_learn = sub.add_parser("learn-theme", help="Learn formatting theme from a WeChat article URL") + p_learn.add_argument("url", help="WeChat article URL") + p_learn.add_argument("--name", required=True, help="Theme name") + args = parser.parse_args() try: @@ -410,6 +424,8 @@ def main(): cmd_image_post(args) elif args.command == "gallery": cmd_gallery(args) + elif args.command == "learn-theme": + cmd_learn_theme(args) except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1)