From 2fa0d7fa6dbc8559643039cf11776ac9ce1198bc Mon Sep 17 00:00:00 2001 From: wangzhuc Date: Sat, 28 Mar 2026 22:53:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=92=E7=89=88=E5=BC=95=E6=93=8E=E5=A4=A7?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=EF=BC=9ACJK=E4=BF=AE=E5=A4=8D=20+=20?= =?UTF-8?q?=E5=A4=96=E9=93=BE=E8=84=9A=E6=B3=A8=20+=20=E6=9A=97=E9=BB=91?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=20+=20=E5=AE=B9=E5=99=A8=E8=AF=AD=E6=B3=95?= =?UTF-8?q?=20+=2016=E4=B8=BB=E9=A2=98=20+=20=E7=94=BB=E5=BB=8AUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit converter.py 新增 6 项能力: - CJK-Latin 自动加空格(中英混排更易读) - 加粗标点外移(修复微信渲染 bug) - ul/ol 转 section(微信原生列表不稳定) - 外链→编号脚注 + 文末参考链接(微信屏蔽外链) - data-darkmode-* 属性注入(适配微信暗黑模式) - :::dialogue / :::timeline / :::callout / :::quote 容器语法 主题系统: - 从 4 个扩充到 16 个(含字节/少数派/报纸/包豪斯/水墨/午夜等风格) - 所有主题新增 darkmode 色值 - 新增 gallery 命令:浏览器内 16 主题并排预览 + 一键复制 Co-Authored-By: Claude Opus 4.6 (1M context) --- toolkit/cli.py | 166 ++++++++++++++ toolkit/converter.py | 306 +++++++++++++++++++++++++ toolkit/themes/bauhaus.yaml | 207 +++++++++++++++++ toolkit/themes/bold-green.yaml | 198 ++++++++++++++++ toolkit/themes/bold-navy.yaml | 197 ++++++++++++++++ toolkit/themes/bytedance.yaml | 199 ++++++++++++++++ toolkit/themes/elegant-rose.yaml | 198 ++++++++++++++++ toolkit/themes/focus-red.yaml | 197 ++++++++++++++++ toolkit/themes/github.yaml | 198 ++++++++++++++++ toolkit/themes/ink.yaml | 204 +++++++++++++++++ toolkit/themes/midnight.yaml | 197 ++++++++++++++++ toolkit/themes/minimal-gold.yaml | 202 ++++++++++++++++ toolkit/themes/minimal.yaml | 9 + toolkit/themes/newspaper.yaml | 206 +++++++++++++++++ toolkit/themes/professional-clean.yaml | 9 + toolkit/themes/sspai.yaml | 198 ++++++++++++++++ toolkit/themes/tech-modern.yaml | 9 + toolkit/themes/warm-editorial.yaml | 9 + 18 files changed, 2909 insertions(+) create mode 100644 toolkit/themes/bauhaus.yaml create mode 100644 toolkit/themes/bold-green.yaml create mode 100644 toolkit/themes/bold-navy.yaml create mode 100644 toolkit/themes/bytedance.yaml create mode 100644 toolkit/themes/elegant-rose.yaml create mode 100644 toolkit/themes/focus-red.yaml create mode 100644 toolkit/themes/github.yaml create mode 100644 toolkit/themes/ink.yaml create mode 100644 toolkit/themes/midnight.yaml create mode 100644 toolkit/themes/minimal-gold.yaml create mode 100644 toolkit/themes/newspaper.yaml create mode 100644 toolkit/themes/sspai.yaml 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