diff --git a/toolkit/cli.py b/toolkit/cli.py index 892ae29..225d540 100644 --- a/toolkit/cli.py +++ b/toolkit/cli.py @@ -142,6 +142,164 @@ def cmd_themes(args): 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", @@ -169,6 +327,12 @@ def main(): # 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: @@ -178,6 +342,8 @@ def main(): 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) diff --git a/toolkit/converter.py b/toolkit/converter.py index 1b1a5ac..a3b07b2 100644 --- a/toolkit/converter.py +++ b/toolkit/converter.py @@ -49,6 +49,12 @@ class WeChatConverter: title = self._extract_title(markdown_text) markdown_text = self._strip_h1(markdown_text) + # Pre-process container blocks (:::dialogue, :::timeline, etc.) + markdown_text = self._preprocess_containers(markdown_text) + + # CJK fix: auto-space between CJK and Latin characters + markdown_text = self._fix_cjk_spacing(markdown_text) + # Parse Markdown → HTML html = self._markdown_to_html(markdown_text) @@ -58,12 +64,24 @@ class WeChatConverter: # Process images (ensure responsive styling) html, images = self._process_images(html) + # CJK fix: move punctuation outside bold tags + html = self._fix_cjk_bold_punctuation(html) + + # CJK fix: convert ul/ol to section-based lists (WeChat renders native lists unreliably) + html = self._convert_lists_to_sections(html) + + # Convert external links to footnotes (WeChat blocks external links) + html = self._convert_links_to_footnotes(html) + # Apply inline CSS from theme html = self._apply_inline_styles(html) # Apply WeChat compatibility fixes html = self._apply_wechat_fixes(html) + # Inject dark mode attributes + html = self._inject_darkmode(html) + # Generate digest from plain text digest = self._generate_digest(html) @@ -201,6 +219,294 @@ class WeChatConverter: return str(soup) + # -- CJK compatibility fixes -- + + def _fix_cjk_spacing(self, text: str) -> str: + """Auto-insert thin space between CJK and Latin/digit characters. + + WeChat renders CJK-Latin without spacing, making mixed text hard to read. + This inserts a thin space (U+200A) at CJK↔Latin boundaries. + Runs on raw Markdown before parsing, skipping code blocks and links. + """ + # CJK unicode ranges + cjk = r'[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]' + latin = r'[A-Za-z0-9]' + + lines = text.split('\n') + result = [] + in_code_block = False + + for line in lines: + if line.strip().startswith('```'): + in_code_block = not in_code_block + result.append(line) + continue + if in_code_block: + result.append(line) + continue + + # CJK followed by Latin + line = re.sub(f'({cjk})({latin})', r'\1 \2', line) + # Latin followed by CJK + line = re.sub(f'({latin})({cjk})', r'\1 \2', line) + result.append(line) + + return '\n'.join(result) + + def _fix_cjk_bold_punctuation(self, html: str) -> str: + """Move Chinese punctuation outside bold/strong tags. + + WeChat renders bold CJK punctuation with ugly spacing. + Move trailing punctuation (,。!?;:、) outside . + """ + # Match: 内容+中文标点内容标点 + pattern = r'()(.*?)([,。!?;:、]+)()' + return re.sub(pattern, r'\1\2\4\3', html) + + def _convert_lists_to_sections(self, html: str) -> str: + """Convert