#!/usr/bin/env python3 """ CLI entry point for WeWrite. Usage: python cli.py preview article.md --theme professional-clean python cli.py publish article.md --appid wx123 --secret abc123 python cli.py themes """ import argparse import sys import webbrowser from pathlib import Path import yaml from converter import WeChatConverter, preview_html from theme import load_theme, list_themes from wechat_api import get_access_token, upload_image, upload_thumb from publisher import create_draft # Config file search order CONFIG_PATHS = [ Path.cwd() / "config.yaml", Path(__file__).parent.parent / "config.yaml", # skill root Path(__file__).parent / "config.yaml", # toolkit dir Path.home() / ".config" / "wewrite" / "config.yaml", ] def load_config() -> dict: """Load config from first found config.yaml.""" for p in CONFIG_PATHS: if p.exists(): with open(p, "r", encoding="utf-8") as f: return yaml.safe_load(f) or {} return {} def cmd_preview(args): """Generate HTML preview and open in browser.""" theme = load_theme(args.theme) converter = WeChatConverter(theme=theme) result = converter.convert_file(args.input) # Wrap in full HTML for browser preview full_html = preview_html(result.html, theme) # Write to temp file input_path = Path(args.input) output = args.output or str(input_path.with_suffix(".html")) Path(output).write_text(full_html, encoding="utf-8") print(f"Title: {result.title}") print(f"Digest: {result.digest}") print(f"Images: {len(result.images)}") print(f"Output: {output}") if not args.no_open: webbrowser.open(f"file://{Path(output).absolute()}") print("Opened in browser.") def cmd_publish(args): """Convert, upload images, and create WeChat draft.""" cfg = load_config() wechat_cfg = cfg.get("wechat", {}) # Resolve from CLI args → config.yaml fallback appid = args.appid or wechat_cfg.get("appid") secret = args.secret or wechat_cfg.get("secret") theme_name = args.theme or cfg.get("theme", "professional-clean") author = args.author or wechat_cfg.get("author") if not appid or not secret: print("Error: --appid and --secret required (or set in config.yaml)", file=sys.stderr) sys.exit(1) theme = load_theme(theme_name) converter = WeChatConverter(theme=theme) result = converter.convert_file(args.input) print(f"Title: {result.title}") print(f"Digest: {result.digest}") print(f"Images found: {len(result.images)}") # Get access token token = get_access_token(appid, secret) print("Access token obtained.") # Upload images referenced in article and replace src # Resolve relative paths against the markdown file's directory md_dir = Path(args.input).resolve().parent html = result.html for img_src in result.images: if img_src.startswith(("http://", "https://")): print(f"Skipping remote image: {img_src}") continue # Try: absolute → relative to CWD → relative to markdown file img_path = Path(img_src) if not img_path.is_absolute(): if not img_path.exists(): img_path = md_dir / img_src if img_path.exists(): print(f"Uploading image: {img_src}") wechat_url = upload_image(token, str(img_path)) html = html.replace(img_src, wechat_url) print(f" -> {wechat_url}") else: print(f"Warning: image not found: {img_src} (searched {md_dir})") # Upload cover image if provided thumb_media_id = None if args.cover: print(f"Uploading cover: {args.cover}") thumb_media_id = upload_thumb(token, args.cover) print(f" -> media_id: {thumb_media_id}") # Create draft title = args.title or result.title or Path(args.input).stem digest = result.digest draft = create_draft( access_token=token, title=title, html=html, digest=digest, thumb_media_id=thumb_media_id, author=author, ) print(f"\nDraft created! media_id: {draft.media_id}") def cmd_themes(args): """List available themes.""" names = list_themes() for name in names: theme = load_theme(name) print(f" {name:24s} {theme.description}") def cmd_gallery(args): """Render all themes side by side in a browser gallery.""" from concurrent.futures import ThreadPoolExecutor # Use provided markdown or a built-in sample if args.input: md_text = Path(args.input).read_text(encoding="utf-8") else: md_text = _gallery_sample_markdown() names = list_themes() results = {} def render_theme(name): theme = load_theme(name) converter = WeChatConverter(theme=theme) result = converter.convert(md_text) return name, theme.description, result.html # Parallel rendering with ThreadPoolExecutor(max_workers=8) as pool: for name, desc, html in pool.map(lambda n: render_theme(n), names): results[name] = (desc, html) # Build gallery HTML gallery_html = _build_gallery_html(results, names) output = args.output or "/tmp/wewrite-gallery.html" Path(output).write_text(gallery_html, encoding="utf-8") print(f"Gallery: {output} ({len(names)} themes)") if not args.no_open: webbrowser.open(f"file://{Path(output).absolute()}") def _gallery_sample_markdown(): return """# 示例文章标题 ## 第一部分 这是一段正常的文章内容,用来展示不同主题的排版效果。WeWrite 支持多种排版主题,每种都有独特的视觉风格。 说实话,选主题这件事——看截图永远不如看实际渲染效果。 ## 关键数据 | 指标 | 数值 | 变化 | |------|------|------| | 阅读量 | 12,580 | +23% | | 分享率 | 4.7% | +0.8% | | 完读率 | 68% | -2% | ## 代码示例 ```python def hello(): print("Hello, WeWrite!") ``` > 好的排版不是让读者注意到设计,而是让读者忘记设计,只记住内容。 ## 列表展示 - 第一个要点:简洁是设计的灵魂 - 第二个要点:一致性比创意更重要 - 第三个要点:移动端体验优先 **加粗文本**和*斜体文本*的样式也需要关注。 最后这段用来展示文章结尾的留白和间距效果。一篇好文章的结尾,应该像一首好歌的最后一个音符——恰到好处地收束。 """ def _join_newline(items): """Join items with comma + newline (workaround for f-string limitation).""" return ",\n".join(items) def _build_gallery_html(results, names): cards = [] for name in names: desc, html = results[name] # Escape for embedding in JS escaped_html = html.replace('\\', '\\\\').replace('`', '\\`').replace('$', '\\$') cards.append(f"""
{name}
{desc}
{html}
""") # Store HTML data for copy data_entries = [] for name in names: desc, html = results[name] safe = html.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') data_entries.append(f" '{name}': '{safe}'") return f""" WeWrite 主题画廊

WeWrite 主题画廊

{len(names)} 个主题 · 点击卡片查看大图 · 点击「复制 HTML」直接粘贴到公众号编辑器

{''.join(cards)}
已复制到剪贴板
""" def main(): parser = argparse.ArgumentParser( prog="wewrite", description="Markdown to WeChat HTML converter and publisher", ) sub = parser.add_subparsers(dest="command", required=True) # preview p_preview = sub.add_parser("preview", help="Generate HTML and open in browser") p_preview.add_argument("input", help="Markdown file path") p_preview.add_argument("-t", "--theme", default="professional-clean", help="Theme name") p_preview.add_argument("-o", "--output", help="Output HTML file path") p_preview.add_argument("--no-open", action="store_true", help="Don't open browser") # publish p_publish = sub.add_parser("publish", help="Convert and publish as WeChat draft") p_publish.add_argument("input", help="Markdown file path") p_publish.add_argument("-t", "--theme", default=None, help="Theme name") p_publish.add_argument("--appid", default=None, help="WeChat AppID (or set in config.yaml)") p_publish.add_argument("--secret", default=None, help="WeChat AppSecret (or set in config.yaml)") p_publish.add_argument("--cover", help="Cover image file path") p_publish.add_argument("--title", help="Override article title") p_publish.add_argument("--author", default=None, help="Article author") # themes sub.add_parser("themes", help="List available themes") # gallery p_gallery = sub.add_parser("gallery", help="Open theme gallery in browser") p_gallery.add_argument("input", nargs="?", default=None, help="Markdown file (optional, uses sample if omitted)") 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") args = parser.parse_args() try: if args.command == "preview": cmd_preview(args) elif args.command == "publish": cmd_publish(args) elif args.command == "themes": cmd_themes(args) elif args.command == "gallery": cmd_gallery(args) except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()