init
This commit is contained in:
815
markdown_editor.py
Normal file
815
markdown_editor.py
Normal file
@@ -0,0 +1,815 @@
|
||||
"""
|
||||
微信 Markdown 编辑器模块
|
||||
支持主题、字体、代码块样式、自定义CSS等功能
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, List, Optional, Callable
|
||||
from dataclasses import dataclass, asdict
|
||||
from pathlib import Path
|
||||
|
||||
import markdown
|
||||
from markdown.extensions import fenced_code, tables, nl2br
|
||||
from pygments import highlight
|
||||
from pygments.lexers import get_lexer_by_name, guess_lexer
|
||||
from pygments.formatters import HtmlFormatter
|
||||
from pygments.styles import get_all_styles
|
||||
from premailer import Premailer
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThemeConfig:
|
||||
"""主题配置"""
|
||||
id: str
|
||||
name: str
|
||||
primary_color: str = "#07C160"
|
||||
background: str = "#ffffff"
|
||||
text_color: str = "#333333"
|
||||
link_color: str = "#576b95"
|
||||
code_bg: str = "#f6f8fa"
|
||||
blockquote_bg: str = "#f9f9f9"
|
||||
blockquote_border: str = "#dfe2e5"
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return asdict(self)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "ThemeConfig":
|
||||
return cls(**data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FontConfig:
|
||||
"""字体配置"""
|
||||
body_font: str = "-apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif"
|
||||
heading_font: str = ""
|
||||
code_font: str = "Consolas, Monaco, 'Courier New', monospace"
|
||||
body_size: int = 16
|
||||
heading_scale: float = 1.3
|
||||
line_height: float = 1.8
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.heading_font:
|
||||
self.heading_font = self.body_font
|
||||
|
||||
|
||||
@dataclass
|
||||
class CodeBlockConfig:
|
||||
"""代码块配置"""
|
||||
theme: str = "default"
|
||||
line_numbers: bool = False
|
||||
background: str = "#f6f8fa"
|
||||
border_radius: int = 6
|
||||
|
||||
# 常用代码高亮主题列表
|
||||
COMMON_THEMES = [
|
||||
"default", "emacs", "friendly", "colorful", "autumn", "murphy", "manni",
|
||||
"monokai", "perldoc", "pastie", "borland", "trac", "native", "fruity",
|
||||
"bw", "vim", "vs", "tango", "rrt", "xcode", "igor", "paraiso-light",
|
||||
"paraiso-dark", "lovelace", "algol", "algol_nu", "arduino", "rainbow_dash",
|
||||
"abap", "solarized-dark", "solarized-light", "sas", "stata", "stata-light",
|
||||
"stata-dark", "inkpot", "zenburn", "gruvbox-dark", "gruvbox-light",
|
||||
"dracula", "one-dark", "material", "nord", "github-dark"
|
||||
]
|
||||
|
||||
@property
|
||||
def available_themes(self) -> List[str]:
|
||||
"""获取所有可用的代码高亮主题"""
|
||||
try:
|
||||
all_themes = list(get_all_styles())
|
||||
return all_themes if all_themes else self.COMMON_THEMES
|
||||
except Exception:
|
||||
return self.COMMON_THEMES
|
||||
|
||||
|
||||
@dataclass
|
||||
class CssScheme:
|
||||
"""CSS 样式方案"""
|
||||
id: str
|
||||
name: str
|
||||
css: str
|
||||
font_config: dict = None
|
||||
code_block_config: dict = None
|
||||
theme_id: str = "default" # 主题 ID
|
||||
primary_color: str = "#07C160" # 主题色
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"css": self.css,
|
||||
"font_config": self.font_config or {},
|
||||
"code_block_config": self.code_block_config or {},
|
||||
"theme_id": self.theme_id,
|
||||
"primary_color": self.primary_color
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "CssScheme":
|
||||
return cls(
|
||||
id=data.get("id", ""),
|
||||
name=data.get("name", ""),
|
||||
css=data.get("css", ""),
|
||||
font_config=data.get("font_config", {}),
|
||||
code_block_config=data.get("code_block_config", {}),
|
||||
theme_id=data.get("theme_id", "default"),
|
||||
primary_color=data.get("primary_color", "#07C160")
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EditorConfig:
|
||||
"""编辑器完整配置"""
|
||||
version: str = "1.0.0"
|
||||
theme: str = "default"
|
||||
custom_themes: List[dict] = None
|
||||
font: FontConfig = None
|
||||
code_block: CodeBlockConfig = None
|
||||
custom_css: str = ""
|
||||
css_schemes: List[dict] = None # CSS 样式方案列表
|
||||
|
||||
def __post_init__(self):
|
||||
if self.custom_themes is None:
|
||||
self.custom_themes = []
|
||||
if self.font is None:
|
||||
self.font = FontConfig()
|
||||
if self.code_block is None:
|
||||
self.code_block = CodeBlockConfig()
|
||||
if self.css_schemes is None:
|
||||
self.css_schemes = []
|
||||
|
||||
|
||||
class ThemeManager:
|
||||
"""主题管理器"""
|
||||
|
||||
# 预设主题
|
||||
PRESET_THEMES = {
|
||||
"default": ThemeConfig(
|
||||
id="default",
|
||||
name="默认主题",
|
||||
primary_color="#07C160",
|
||||
background="#ffffff",
|
||||
text_color="#333333"
|
||||
),
|
||||
"elegant": ThemeConfig(
|
||||
id="elegant",
|
||||
name="优雅主题",
|
||||
primary_color="#2c3e50",
|
||||
background="#fafafa",
|
||||
text_color="#2c3e50"
|
||||
),
|
||||
"tech": ThemeConfig(
|
||||
id="tech",
|
||||
name="科技主题",
|
||||
primary_color="#3498db",
|
||||
background="#1e1e1e",
|
||||
text_color="#d4d4d4",
|
||||
code_bg="#2d2d2d"
|
||||
),
|
||||
"literary": ThemeConfig(
|
||||
id="literary",
|
||||
name="文艺主题",
|
||||
primary_color="#8b4513",
|
||||
background="#fefcf8",
|
||||
text_color="#4a4a4a"
|
||||
),
|
||||
"festive": ThemeConfig(
|
||||
id="festive",
|
||||
name="节日主题",
|
||||
primary_color="#e74c3c",
|
||||
background="#fff5f5",
|
||||
text_color="#c0392b"
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, config_dir: str = None):
|
||||
if config_dir:
|
||||
self.config_dir = config_dir
|
||||
else:
|
||||
# 获取程序运行目录(兼容 exe 和普通 Python 运行)
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 如果是打包后的 exe,使用 exe 所在目录
|
||||
app_dir = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# 如果是普通 Python 运行,使用脚本所在目录
|
||||
app_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
self.config_dir = os.path.join(app_dir, "data", "editor")
|
||||
|
||||
os.makedirs(self.config_dir, exist_ok=True)
|
||||
self.config_file = os.path.join(self.config_dir, "editor_config.json")
|
||||
print(f"[ThemeManager] 配置目录: {self.config_dir}")
|
||||
print(f"[ThemeManager] 配置文件: {self.config_file}")
|
||||
self.config = self._load_config()
|
||||
self._custom_themes: Dict[str, ThemeConfig] = {}
|
||||
self._css_schemes: Dict[str, CssScheme] = {}
|
||||
self._load_custom_themes()
|
||||
self._load_css_schemes()
|
||||
|
||||
def _load_config(self) -> EditorConfig:
|
||||
"""加载配置"""
|
||||
if os.path.exists(self.config_file):
|
||||
try:
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return self._parse_config(data)
|
||||
except Exception as e:
|
||||
print(f"[ThemeManager] 加载配置失败: {e}")
|
||||
return EditorConfig()
|
||||
|
||||
def _parse_config(self, data: dict) -> EditorConfig:
|
||||
"""解析配置数据"""
|
||||
config = EditorConfig(
|
||||
version=data.get("version", "1.0.0"),
|
||||
theme=data.get("theme", "default"),
|
||||
custom_themes=data.get("custom_themes", []),
|
||||
custom_css=data.get("custom_css", "")
|
||||
)
|
||||
|
||||
# 解析字体配置
|
||||
if "font" in data:
|
||||
font_data = data["font"]
|
||||
config.font = FontConfig(
|
||||
body_font=font_data.get("body_font", config.font.body_font),
|
||||
heading_font=font_data.get("heading_font", config.font.heading_font),
|
||||
code_font=font_data.get("code_font", config.font.code_font),
|
||||
body_size=font_data.get("body_size", config.font.body_size),
|
||||
heading_scale=font_data.get("heading_scale", config.font.heading_scale),
|
||||
line_height=font_data.get("line_height", config.font.line_height)
|
||||
)
|
||||
|
||||
# 解析代码块配置
|
||||
if "code_block" in data:
|
||||
code_data = data["code_block"]
|
||||
config.code_block = CodeBlockConfig(
|
||||
theme=code_data.get("theme", config.code_block.theme),
|
||||
line_numbers=code_data.get("line_numbers", config.code_block.line_numbers),
|
||||
background=code_data.get("background", config.code_block.background),
|
||||
border_radius=code_data.get("border_radius", config.code_block.border_radius)
|
||||
)
|
||||
|
||||
# 解析 CSS 方案
|
||||
if "css_schemes" in data:
|
||||
config.css_schemes = data["css_schemes"]
|
||||
|
||||
return config
|
||||
|
||||
def _load_custom_themes(self):
|
||||
"""加载自定义主题"""
|
||||
for theme_data in self.config.custom_themes:
|
||||
try:
|
||||
theme = ThemeConfig.from_dict(theme_data)
|
||||
self._custom_themes[theme.id] = theme
|
||||
except Exception as e:
|
||||
print(f"[ThemeManager] 加载自定义主题失败: {e}")
|
||||
|
||||
def _load_css_schemes(self):
|
||||
"""加载 CSS 样式方案"""
|
||||
for scheme_data in self.config.css_schemes:
|
||||
try:
|
||||
scheme = CssScheme.from_dict(scheme_data)
|
||||
self._css_schemes[scheme.id] = scheme
|
||||
except Exception as e:
|
||||
print(f"[ThemeManager] 加载 CSS 方案失败: {e}")
|
||||
|
||||
def save_config(self):
|
||||
"""保存配置"""
|
||||
data = {
|
||||
"version": self.config.version,
|
||||
"theme": self.config.theme,
|
||||
"custom_themes": [t.to_dict() for t in self._custom_themes.values()],
|
||||
"font": {
|
||||
"body_font": self.config.font.body_font,
|
||||
"heading_font": self.config.font.heading_font,
|
||||
"code_font": self.config.font.code_font,
|
||||
"body_size": self.config.font.body_size,
|
||||
"heading_scale": self.config.font.heading_scale,
|
||||
"line_height": self.config.font.line_height
|
||||
},
|
||||
"code_block": {
|
||||
"theme": self.config.code_block.theme,
|
||||
"line_numbers": self.config.code_block.line_numbers,
|
||||
"background": self.config.code_block.background,
|
||||
"border_radius": self.config.code_block.border_radius
|
||||
},
|
||||
"custom_css": self.config.custom_css,
|
||||
"css_schemes": [s.to_dict() for s in self._css_schemes.values()]
|
||||
}
|
||||
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
def get_all_themes(self) -> List[ThemeConfig]:
|
||||
"""获取所有主题(预设+自定义)"""
|
||||
themes = list(self.PRESET_THEMES.values())
|
||||
themes.extend(self._custom_themes.values())
|
||||
return themes
|
||||
|
||||
def get_theme(self, theme_id: str) -> Optional[ThemeConfig]:
|
||||
"""获取指定主题"""
|
||||
if theme_id in self.PRESET_THEMES:
|
||||
return self.PRESET_THEMES[theme_id]
|
||||
return self._custom_themes.get(theme_id)
|
||||
|
||||
def get_current_theme(self) -> ThemeConfig:
|
||||
"""获取当前主题"""
|
||||
return self.get_theme(self.config.theme) or self.PRESET_THEMES["default"]
|
||||
|
||||
def set_theme(self, theme_id: str):
|
||||
"""设置当前主题"""
|
||||
if theme_id in self.PRESET_THEMES or theme_id in self._custom_themes:
|
||||
self.config.theme = theme_id
|
||||
self.save_config()
|
||||
|
||||
def add_custom_theme(self, theme: ThemeConfig):
|
||||
"""添加自定义主题"""
|
||||
self._custom_themes[theme.id] = theme
|
||||
self.save_config()
|
||||
|
||||
def remove_custom_theme(self, theme_id: str):
|
||||
"""删除自定义主题"""
|
||||
if theme_id in self._custom_themes:
|
||||
del self._custom_themes[theme_id]
|
||||
if self.config.theme == theme_id:
|
||||
self.config.theme = "default"
|
||||
self.save_config()
|
||||
|
||||
# ========== CSS 样式方案管理 ==========
|
||||
|
||||
def get_all_css_schemes(self) -> List[CssScheme]:
|
||||
"""获取所有 CSS 样式方案"""
|
||||
return list(self._css_schemes.values())
|
||||
|
||||
def get_css_scheme(self, scheme_id: str) -> Optional[CssScheme]:
|
||||
"""获取指定 CSS 方案"""
|
||||
return self._css_schemes.get(scheme_id)
|
||||
|
||||
def add_css_scheme(self, scheme: CssScheme):
|
||||
"""添加 CSS 样式方案"""
|
||||
self._css_schemes[scheme.id] = scheme
|
||||
self.save_config()
|
||||
|
||||
def remove_css_scheme(self, scheme_id: str):
|
||||
"""删除 CSS 样式方案"""
|
||||
if scheme_id in self._css_schemes:
|
||||
del self._css_schemes[scheme_id]
|
||||
self.save_config()
|
||||
|
||||
def apply_css_scheme(self, scheme_id: str) -> bool:
|
||||
"""应用 CSS 样式方案"""
|
||||
scheme = self._css_schemes.get(scheme_id)
|
||||
if not scheme:
|
||||
return False
|
||||
|
||||
# 应用主题
|
||||
if scheme.theme_id:
|
||||
self.config.theme = scheme.theme_id
|
||||
|
||||
# 应用主题色(更新当前主题的主色调)
|
||||
if scheme.primary_color and scheme.theme_id:
|
||||
# 先从预设主题中查找,再从自定义主题中查找
|
||||
theme = self.PRESET_THEMES.get(scheme.theme_id)
|
||||
if not theme:
|
||||
theme = self._custom_themes.get(scheme.theme_id)
|
||||
if theme:
|
||||
theme.primary_color = scheme.primary_color
|
||||
|
||||
# 应用 CSS
|
||||
self.config.custom_css = scheme.css
|
||||
|
||||
# 应用字体配置
|
||||
if scheme.font_config:
|
||||
for key, value in scheme.font_config.items():
|
||||
if hasattr(self.config.font, key):
|
||||
setattr(self.config.font, key, value)
|
||||
|
||||
# 应用代码块配置
|
||||
if scheme.code_block_config:
|
||||
for key, value in scheme.code_block_config.items():
|
||||
if hasattr(self.config.code_block, key):
|
||||
setattr(self.config.code_block, key, value)
|
||||
|
||||
self.save_config()
|
||||
return True
|
||||
|
||||
|
||||
class MarkdownToWechatConverter:
|
||||
"""Markdown 转微信公众号 HTML 转换器"""
|
||||
|
||||
def __init__(self, theme_manager: ThemeManager = None):
|
||||
self.theme_manager = theme_manager or ThemeManager()
|
||||
|
||||
def convert(self, md_content: str) -> str:
|
||||
"""转换 Markdown 为微信公众号 HTML"""
|
||||
config = self.theme_manager.config
|
||||
theme = self.theme_manager.get_current_theme()
|
||||
|
||||
# 1. 转换 Markdown 为 HTML
|
||||
html_body = self._markdown_to_html(md_content, config.code_block.theme)
|
||||
|
||||
# 2. 生成完整 HTML
|
||||
html = self._generate_full_html(html_body, theme, config)
|
||||
|
||||
return html
|
||||
|
||||
def _markdown_to_html(self, md_content: str, code_theme: str) -> str:
|
||||
"""Markdown 转 HTML"""
|
||||
# 配置 Markdown 扩展
|
||||
extensions = [
|
||||
'markdown.extensions.fenced_code',
|
||||
'markdown.extensions.tables',
|
||||
'markdown.extensions.nl2br',
|
||||
]
|
||||
|
||||
# 预处理:确保空行正确(Markdown 需要空行来分隔段落)
|
||||
# 将多个连续换行符转换为两个换行符(段落分隔)
|
||||
import re
|
||||
md_content = re.sub(r'\n{3,}', '\n\n', md_content)
|
||||
|
||||
# 使用 Pygments 进行代码高亮
|
||||
md = markdown.Markdown(extensions=extensions)
|
||||
html = md.convert(md_content)
|
||||
|
||||
# 处理代码块高亮
|
||||
html = self._highlight_code_blocks(html, code_theme)
|
||||
|
||||
# 后处理:为段落之间添加额外的空行
|
||||
# 在段落之间插入空行(使用 <p> </p> 或 <br>)
|
||||
# 将 </p><p> 替换为 </p><p> </p><p> 来添加空行
|
||||
html = re.sub(r'</p>\s*<p>', '</p><p> </p><p>', html)
|
||||
|
||||
return html
|
||||
|
||||
def _highlight_code_blocks(self, html: str, theme: str) -> str:
|
||||
"""高亮代码块"""
|
||||
import re
|
||||
from pygments.styles import get_style_by_name
|
||||
|
||||
# 验证主题是否可用,如果不可用则使用默认主题
|
||||
try:
|
||||
get_style_by_name(theme)
|
||||
except:
|
||||
theme = "default"
|
||||
|
||||
# 查找代码块
|
||||
pattern = r'<pre><code(?:\s+class="language-([^"]*)")?>([^<]*)</code></pre>'
|
||||
|
||||
def replace_code_block(match):
|
||||
lang = match.group(1) or 'text'
|
||||
code = match.group(2)
|
||||
|
||||
# 解码 HTML 实体
|
||||
code = code.replace('<', '<').replace('>', '>').replace('&', '&')
|
||||
|
||||
try:
|
||||
lexer = get_lexer_by_name(lang)
|
||||
except:
|
||||
lexer = guess_lexer(code)
|
||||
|
||||
try:
|
||||
formatter = HtmlFormatter(
|
||||
style=theme,
|
||||
noclasses=True,
|
||||
cssclass='highlight'
|
||||
)
|
||||
highlighted = highlight(code, lexer, formatter)
|
||||
except Exception:
|
||||
# 如果高亮失败,返回原始代码
|
||||
highlighted = f'<pre><code>{code}</code></pre>'
|
||||
|
||||
return f'<div class="code-block-wrapper">{highlighted}</div>'
|
||||
|
||||
return re.sub(pattern, replace_code_block, html, flags=re.DOTALL)
|
||||
|
||||
def _generate_full_html(self, html_body: str, theme: ThemeConfig, config: EditorConfig) -> str:
|
||||
"""生成完整 HTML 文档"""
|
||||
|
||||
# 基础样式
|
||||
base_css = f"""
|
||||
<style>
|
||||
.wechat-article {{
|
||||
max-width: 677px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: {config.font.body_font};
|
||||
font-size: {config.font.body_size}px;
|
||||
line-height: {config.font.line_height};
|
||||
color: {theme.text_color};
|
||||
background: {theme.background};
|
||||
}}
|
||||
|
||||
.wechat-article h1 {{
|
||||
font-size: {config.font.body_size * config.font.heading_scale * 1.5:.0f}px;
|
||||
font-weight: bold;
|
||||
margin: 1.5em 0 0.8em;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 2px solid {theme.primary_color};
|
||||
color: {theme.primary_color};
|
||||
}}
|
||||
|
||||
.wechat-article h2 {{
|
||||
font-size: {config.font.body_size * config.font.heading_scale * 1.3:.0f}px;
|
||||
font-weight: bold;
|
||||
margin: 1.3em 0 0.6em;
|
||||
padding-left: 12px;
|
||||
border-left: 4px solid {theme.primary_color};
|
||||
}}
|
||||
|
||||
.wechat-article h3 {{
|
||||
font-size: {config.font.body_size * config.font.heading_scale * 1.1:.0f}px;
|
||||
font-weight: bold;
|
||||
margin: 1em 0 0.5em;
|
||||
color: {theme.primary_color};
|
||||
}}
|
||||
|
||||
.wechat-article p {{
|
||||
margin: 1em 0;
|
||||
text-align: justify;
|
||||
}}
|
||||
|
||||
.wechat-article a {{
|
||||
color: {theme.link_color};
|
||||
text-decoration: none;
|
||||
}}
|
||||
|
||||
.wechat-article img {{
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 1em auto;
|
||||
}}
|
||||
|
||||
.wechat-article blockquote {{
|
||||
margin: 1em 0;
|
||||
padding: 12px 16px;
|
||||
background: {theme.blockquote_bg};
|
||||
border-left: 4px solid {theme.blockquote_border};
|
||||
color: #6a737d;
|
||||
}}
|
||||
|
||||
.wechat-article ul, .wechat-article ol {{
|
||||
margin: 1em 0;
|
||||
padding-left: 2em;
|
||||
}}
|
||||
|
||||
.wechat-article li {{
|
||||
margin: 0.3em 0;
|
||||
}}
|
||||
|
||||
.wechat-article code {{
|
||||
font-family: {config.font.code_font};
|
||||
background: {theme.code_bg};
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
|
||||
.wechat-article pre {{
|
||||
font-family: {config.font.code_font};
|
||||
background: {config.code_block.background};
|
||||
padding: 16px;
|
||||
border-radius: {config.code_block.border_radius}px;
|
||||
overflow-x: auto;
|
||||
margin: 1em 0;
|
||||
}}
|
||||
|
||||
.wechat-article pre code {{
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}}
|
||||
|
||||
.wechat-article table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1em 0;
|
||||
}}
|
||||
|
||||
.wechat-article th, .wechat-article td {{
|
||||
border: 1px solid #dfe2e5;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}}
|
||||
|
||||
.wechat-article th {{
|
||||
background: #f6f8fa;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
.wechat-article tr:nth-child(even) {{
|
||||
background: #fafafa;
|
||||
}}
|
||||
|
||||
{config.custom_css}
|
||||
</style>
|
||||
"""
|
||||
|
||||
full_html = f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>微信公众号文章</title>
|
||||
{base_css}
|
||||
</head>
|
||||
<body>
|
||||
<div class="wechat-article">
|
||||
{html_body}
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
# 使用 premailer 将 CSS 内联到 HTML 元素中(微信公众号需要内联样式)
|
||||
try:
|
||||
premailer = Premailer(full_html, remove_classes=False)
|
||||
inlined_html = premailer.transform()
|
||||
return inlined_html
|
||||
except Exception as e:
|
||||
print(f"[MarkdownToWechatConverter] CSS 内联失败: {e}")
|
||||
return full_html
|
||||
|
||||
def export_css(self) -> str:
|
||||
"""导出当前主题 CSS"""
|
||||
theme = self.theme_manager.get_current_theme()
|
||||
config = self.theme_manager.config
|
||||
|
||||
css = f"""/* 微信公众号文章样式 - {theme.name} */
|
||||
.wechat-article {{
|
||||
max-width: 677px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: {config.font.body_font};
|
||||
font-size: {config.font.body_size}px;
|
||||
line-height: {config.font.line_height};
|
||||
color: {theme.text_color};
|
||||
background: {theme.background};
|
||||
}}
|
||||
|
||||
.wechat-article h1 {{
|
||||
font-size: {config.font.body_size * config.font.heading_scale * 1.5:.0f}px;
|
||||
font-weight: bold;
|
||||
margin: 1.5em 0 0.8em;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 2px solid {theme.primary_color};
|
||||
color: {theme.primary_color};
|
||||
}}
|
||||
|
||||
.wechat-article h2 {{
|
||||
font-size: {config.font.body_size * config.font.heading_scale * 1.3:.0f}px;
|
||||
font-weight: bold;
|
||||
margin: 1.3em 0 0.6em;
|
||||
padding-left: 12px;
|
||||
border-left: 4px solid {theme.primary_color};
|
||||
}}
|
||||
|
||||
.wechat-article h3 {{
|
||||
font-size: {config.font.body_size * config.font.heading_scale * 1.1:.0f}px;
|
||||
font-weight: bold;
|
||||
margin: 1em 0 0.5em;
|
||||
color: {theme.primary_color};
|
||||
}}
|
||||
|
||||
.wechat-article p {{
|
||||
margin: 1em 0;
|
||||
text-align: justify;
|
||||
}}
|
||||
|
||||
.wechat-article a {{
|
||||
color: {theme.link_color};
|
||||
text-decoration: none;
|
||||
}}
|
||||
|
||||
.wechat-article img {{
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 1em auto;
|
||||
}}
|
||||
|
||||
.wechat-article blockquote {{
|
||||
margin: 1em 0;
|
||||
padding: 12px 16px;
|
||||
background: {theme.blockquote_bg};
|
||||
border-left: 4px solid {theme.blockquote_border};
|
||||
color: #6a737d;
|
||||
}}
|
||||
|
||||
.wechat-article ul, .wechat-article ol {{
|
||||
margin: 1em 0;
|
||||
padding-left: 2em;
|
||||
}}
|
||||
|
||||
.wechat-article li {{
|
||||
margin: 0.3em 0;
|
||||
}}
|
||||
|
||||
.wechat-article code {{
|
||||
font-family: {config.font.code_font};
|
||||
background: {theme.code_bg};
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
|
||||
.wechat-article pre {{
|
||||
font-family: {config.font.code_font};
|
||||
background: {config.code_block.background};
|
||||
padding: 16px;
|
||||
border-radius: {config.code_block.border_radius}px;
|
||||
overflow-x: auto;
|
||||
margin: 1em 0;
|
||||
}}
|
||||
|
||||
.wechat-article pre code {{
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}}
|
||||
|
||||
.wechat-article table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1em 0;
|
||||
}}
|
||||
|
||||
.wechat-article th, .wechat-article td {{
|
||||
border: 1px solid #dfe2e5;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}}
|
||||
|
||||
.wechat-article th {{
|
||||
background: #f6f8fa;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
.wechat-article tr:nth-child(even) {{
|
||||
background: #fafafa;
|
||||
}}
|
||||
"""
|
||||
return css
|
||||
|
||||
|
||||
class WechatMarkdownEditor:
|
||||
"""微信 Markdown 编辑器主类"""
|
||||
|
||||
def __init__(self):
|
||||
self.theme_manager = ThemeManager()
|
||||
self.converter = MarkdownToWechatConverter(self.theme_manager)
|
||||
|
||||
def render(self, md_content: str) -> str:
|
||||
"""渲染 Markdown 为 HTML"""
|
||||
return self.converter.convert(md_content)
|
||||
|
||||
def get_config(self) -> EditorConfig:
|
||||
"""获取编辑器配置"""
|
||||
return self.theme_manager.config
|
||||
|
||||
def save_config(self):
|
||||
"""保存配置"""
|
||||
self.theme_manager.save_config()
|
||||
|
||||
def update_font_config(self, **kwargs):
|
||||
"""更新字体配置"""
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(self.theme_manager.config.font, key):
|
||||
setattr(self.theme_manager.config.font, key, value)
|
||||
self.save_config()
|
||||
|
||||
def update_code_block_config(self, **kwargs):
|
||||
"""更新代码块配置"""
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(self.theme_manager.config.code_block, key):
|
||||
setattr(self.theme_manager.config.code_block, key, value)
|
||||
self.save_config()
|
||||
|
||||
def update_custom_css(self, css: str):
|
||||
"""更新自定义 CSS"""
|
||||
self.theme_manager.config.custom_css = css
|
||||
self.save_config()
|
||||
|
||||
def export_html(self, md_content: str, output_path: str):
|
||||
"""导出 HTML 文件"""
|
||||
html = self.render(md_content)
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(html)
|
||||
|
||||
def export_css(self, output_path: str):
|
||||
"""导出 CSS 文件"""
|
||||
css = self.converter.export_css()
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(css)
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def create_editor() -> WechatMarkdownEditor:
|
||||
"""创建编辑器实例"""
|
||||
return WechatMarkdownEditor()
|
||||
|
||||
|
||||
def markdown_to_wechat(md_content: str, theme_id: str = "default") -> str:
|
||||
"""快速转换 Markdown 为微信公众号 HTML"""
|
||||
editor = create_editor()
|
||||
editor.theme_manager.set_theme(theme_id)
|
||||
return editor.render(md_content)
|
||||
Reference in New Issue
Block a user