猫工具站
帮助中心 猫Blog 关于我们 广告合作 EN

JSON、转义与 Markdown 预览:四个格式页面的能力边界,和它们彼此不一致的地方

格式更新于 2026-09-30

四个页面都归在「格式」下面,但没有两台机器是一样的。JSON 格式化是 JSON.parse 接 JSON.stringify 的一层壳;JSON 转代码是一台命名规则机;HTML 转义只处理五个字符;Markdown 预览则是四个里唯一真的把文本重排成 HTML 的那个——也因此是唯一一个「渲染即执行」的。

知道每台机器是什么,比背按钮位置有用:一个数被改写了,你只需要问这台机器会不会改它;一条报错没出现,你只需要问这台机器的失败路径长什么样。

下面每一条都是从这四个页面自己的脚本里读出来、再在页面上实际跑一遍确认过的。

格式化页就是 JSON.stringify,不多也不少

JSON 在线格式化的两个主按钮是「格式化」和「压缩」。页面上没有缩进下拉、没有键排序、没有保留键序的开关——因为它根本没实现这些。所谓格式化是 JSON.stringify(obj, null, 2),压缩是同一句去掉第二个参数。所以 stringify 的毛病它全都有,而且都是对的:

你粘进去 页面上出现 为什么
{"b":1,"a":2,"b":3} "b": 3 在前,"a": 2 在后 键序按解析结果,重复键保第一个的位置、取最后的值
{"a":1e309} "a": null,并提示格式化成功 JSON.parse 给 Infinity,JSON.stringify 没有对应写法,只写 null
{"id":12345678901234567890} "id": 12345678901234567000 数字走 IEEE 754 双精度,尾数存不下 20 位整数
{"a":"\\u00e9"} "a": "é" 反斜杠 u 转义在解析时被解成字符,输出里不再保留转义写法
// 注 加对象 红条报错 JSON 不允许注释
{'a':1} 红条报错 字符串必须双引号
0 / false 0 / false 标量也是合法 JSON,能正常输出

尾逗号、单引号、注释,以及你从别的语言里带出来的非 JSON 字面量(NaN、undefined)——这四类占了被拒原因的绝大部分。

「字符数」那一行数字的是 JavaScript 的字符串长度,不是字节。实测 {"a":"中文中文中文"}:输入 14 个字符、UTF-8 编码 26 字节,格式化后输出 19 个字符(缩进和换行加了 7 个字符,三个中文每字在线上是 3 字节、在这里算 1 个字符)。所以这个页面的数字和你 HTTP 客户端里的数字不一样,别用它估载荷上限。

失败路径是这个页面最该记的一段

「格式化 / 压缩 / 清空」三个按钮开头都会先把两条横幅收起来,然后:

  • 输入是空的或只有空格:红条 请输入 JSON 文本。输出框不动。
  • JSON 非法:红条 JSON 解析错误: 加引擎原话。输出框保留着上一次的结果,「字符数」也还是上一次的数字。实测:先格式化 {"a":1} 得到 12 个字符的输出,再格式化 {"a":,},红条挂上来,输出框里仍是那个 {\n "a": 1\n}、字符数仍写着 12。绿条此时是被显式隐藏的,不会和你对着显示。
  • 输入恰好是 null:这个页面完全不动。JSON.parse('null') 成功返回 null,而 null 正是页面用来表示「解析失败」的哨兵值,于是判断 if (obj === null) return; 把它当失败处理,又因为失败提示已经在 validateAndGet 里给过了(这次没给),结果是:没有红条、没有绿条、输出不变、字符数不变。实测过:上一轮输出还在,横幅两条都收着。而字符串 null(写成 "null")是另一种东西,它正常输出 "null"、正常报成功。
  • 你随后在输入框里改了一个字:这个页面没有给输入框挂监听,所以红条会一直挂在屏幕上,直到你再点一次任意按钮(那时它先被收起来)。换句话说:屏幕上没有消息不等于成功。想知道刚才那一下到底成没成,看输出框和字符数有没有变。

这条和英文页的行为不一样——英文页解析失败会把输出清空、并在你输入时收起横幅。这两个页面上,同一个动作留下的线索不同,跨语言用的时候别照搬经验。

关于红条里那句引擎原话:措辞是浏览器给的,会跟着你浏览器的语言和版本变(我这台英文环境的 Edge 给的是英文整句),但里面的位置数字是可靠的定位线索。同一台机器上跑出来的几条:

你粘进去 红条全文
{'a':1} JSON 解析错误:Expected property name or '}' in JSON at position 1 (line 1 column 2)
{"a":,} JSON 解析错误:Unexpected token ',', "{"a":,}" is not valid JSON
// 注 加对象 JSON 解析错误:Unexpected token '/', "// …" is not valid JSON
{"a":NaN} JSON 解析错误:Unexpected token 'N', "{"a":NaN}" is not valid JSON

第一句里的 position 1 (line 1 column 2) 指向的就是那个单引号。

转代码页:两种语言,一条命名规则

JSON 转代码的下拉里正好两个选项:TypeScript interface 和 Python dict。没有根名输入框、没有分号开关、没有「给每个对象都生成类型」。

TypeScript 模式的规则(以下都是实测输出):

  • 顶层对象永远叫 interface Root { … },改不了。
  • 嵌套对象按键名转 PascalCase:"user_profile" → interface UserProfile,"http_response_code" → interface HttpResponseCode。
  • 对象数组在类型名后加 Item:{"list":[{"x":1}]} → list: ListItem[] 外加 interface ListItem { x: number; }。
  • 键名本身原样抄进产物:{"a-b":1} → a-b: number;;带空格的键也照抄。这不是合法 TypeScript,页面不会替你把它们变成 aB 或加引号。
  • 只有大小写不同的键会撞名:{"a":{"x":1},"A":{"x":"s"}} 生成两个都叫 interface A 的定义,内容还不一样。my_key 和 myKey、my key 同理(都变 MyKey)。
  • 数组类型只看第 0 个元素,后面的根本不检查:[1,"a"] → number[];[[1,2],[3]] → number[][];空数组 [] 和第 0 个是 null 的数组 → any[]。
  • 顶层就是数组时,下标被当成键:[{"a":1},{"a":2}] → 0: _0; 1: _1; 加两个 interface。
  • 顶层是标量时你会拿到一个空壳:输入 42 → interface Root {\n\n},合法但不含任何信息;这不报错,也没有提示。

Python 模式产出的是 data = { … },四空格缩进、键用双引号。它不是 Python 字面量:实测 {"ok":true,"miss":null,"big":1e309,"empty":{}} 得到

data = {
    "ok": true,
    "miss": None,
    "big": Infinity,
    "empty": {}
}

null 被换成了 None,但 true、false、Infinity 都按 JSON 的写法留着——Python 里应是 True、False、float("inf")。这段代码贴进 .py 文件会在第一行就 NameError。它是给人眼看的,不是给人跑的。

失败路径:空输入给 请输入 JSON 文本,解析失败给 JSON 解析错误: 加引擎原话,输出框保留上一次的代码(实测:先生成 interface Root { a: number; },再喂 {oops},那块里仍是上一轮的代码)。按钮是「生成代码 / 清空 / 复制代码」。

转义页只动五个字符,层数由「交换」控制

HTML 转义的转义是一条五步链:&→&amp;、<→&lt;、>→&gt;、"→&quot;、'→&#39;;反转义按同样的顺序把五个实体换回去。就这些。

实测样本:

输入 转义后 反转义后
<a href="x">&amp;</a> &lt;a href=&quot;x&quot;&gt;&amp;amp;&lt;/a&gt; <a href="x">&</a>
&nbsp;&copy;&eacute; &amp;nbsp;&amp;copy;&amp;eacute; &nbsp;&copy;&eacute; 原样
&#x27;&amp; &amp;#x27;&amp;amp; &#x27;&
© ü © ü © ü

&copy;、&eacute;、&#x27; 这类命名/数字实体两个方向都不认识。它不是实体解码器,是一个「五个字符的标签安全过滤器」。适合把代码片段贴进博客正文或评论区,不适合把一整个被反复转义过的文档还原。

要点:按两次不等于转两层。 两个按钮都是从「输入框」取原文、往「输出框」写结果,所以连点两次「转义」得到的仍是同一份一层结果——实测 <b>&amp;</b> 连点两次还是 &lt;b&gt;&amp;amp;&lt;/b&gt;。想再加一层,要在两次之间点「交换」:交换会把两个框的内容对调,于是下一轮「转义」读到的就是刚才的结果,产出 &amp;lt;b&amp;gt;&amp;amp;amp;&amp;lt;/b&amp;gt;。

顺带一句:刚点完交换的那一刻,输出框里放的是你原来的文本,不是任何结果——它只是把两个框调了个个儿,得再按一次按钮才有对应关系。反方向同理:&amp;lt;p&amp;gt; 点一次「反转义」得 &lt;p&gt;,再点还是 &lt;p&gt;,点交换后再点一次才得到 <p>。层数是靠交换推进的,不是靠多点。

「字符数」跟着输出框走,静态写的和 JS 写的是同一个标签 字符数:,不会出现按完按钮标签换了个说法的情况。而且已经转义过的文本再转一次是常见事故:&nbsp; 会变成 &amp;nbsp;,贴到你的页面上就显示成字面的 &nbsp; 而不是一个空格。从 CMS 里复制出来的内容往往已经转过了,这里再转一遍是 bug,不是修复。

预览页:本站语法覆盖最全的一个,但代码块和引号靠不住

Markdown 在线预览左侧打字右侧立刻渲染(输入框有监听),滚动会跟着同步。它支持什么、不支持什么,页面 FAQ 写得大体准确,实测之后可以给出更硬的边界。

支持的(括号里是实测得到的 DOM):

  • 标题 h1 到 h6:###### 六级 → <h6>六级</h6>
  • 分割线:独占一行的 --- → <hr>
  • 引用:> 开头的行 → <blockquote>,连续多行会被并成同一个引用块、中间插 <br>
  • 无序列表 - → <ul><li>;有序列表 1. → <ol><li>
  • 围栏代码块 → <pre><code>;行内 `x` → <code>
  • 粗体 **x**、斜体 *x*、粗斜 ***x***
  • 链接 [文字](URL) → <a href="URL" target="_blank" rel="noopener">
  • 其余每个非空行包成 <p>

不支持的:表格、任务列表、脚注(FAQ 自己承认)。实测表格的每一行都变成独立段落、竖线原样留着:|a|b| → <p>|a|b|</p>;图片也不支持,![alt](http://e.com/x.png) 渲染成 !<a href="…">alt</a>——链接生成了,前面还多掉一个感叹号。

围栏语言标注被丢弃。 ```python 和 ```js 都只产出 <pre><code>,没有 class、没有任何高亮钩子——正则捕获了语言名但没用它。指望这个页面出带高亮的 HTML 是不可能的。

更要紧的是替换顺序:标题、分割线、引用、列表先跑,围栏后跑。 于是代码块里的内容在被识别成代码之前,已经先被当作 Markdown 处理过了。实测这段:

```
# 这不是标题
- 这不是列表
**这不是粗体**
[这不是链接](http://e.com)
```

得到的 DOM 是 <pre><code><h1>这不是标题</h1><ul><li>这不是列表</li></ul><p><strong>这不是粗体</strong></p></code><p><code><a href="…" …>这不是链接</a></code></p></pre><p></p> —— 井号变成了 <h1>、- 变成了列表、** 变粗,而且结构整个塌了:<pre> 里面冒出 <p>,末尾还多出一个空的 <p>。所以别把带 # 注释或 YAML 片段的代码直接贴进这个预览,它不会保持原样。

角度括号转义了,引号没有。 页面先做 escapeHtml,而它只替换 &、<、> 三个字符——所以 <script>alert(1)</script> 会变成字面文本(实测得到 &lt;script&gt;…),但链接规则是把 URL 直接塞进 href="$2",URL 里出现一个 " 就能开出新属性。实测:

[点我](x" onclick="window.__pwn=1)

渲染成 <a href="x" onclick="window.__pwn=1" target="_blank" rel="noopener">点我</a>,我程序化地点击它之后,window.__pwn 真的变成了 1。同理 javascript: 开头的 href 会原样保留,跟着链接一按就执行。

这不是「Markdown 写错」的问题,而是这个页面的定位问题:它把渲染当成执行。自己写的笔记随便用;任何别人给你的 Markdown,别在这个页面里渲染,尤其是会点它的场景。

两个字数框永远显示同一个数字。 渲染完之后,页面把 md.length 同时写给「编辑区」和「预览」两个计数标签——实测输入 # hi there 两边都是 10 字。所以右边那个不是渲染后的文字数,它只是源码长度又显示了一遍,删掉多少 Markdown 符号都不会变。而这里的「字」实际是 JavaScript 的字符串长度(UTF-16 单元):输入三个 😀 会显示 6 字,因为一个 emoji 占两个单元。

哪个页面干哪件事

你有什么 用 要当心
一坨压缩 JSON 要看 格式化 大整数被折、Infinity 变 null 还报成功
要贴回线上的 JSON 格式化的「压缩」 键序按解析结果,不是你文件里的顺序
一份响应体要照着写类型 转代码的 TypeScript 带空格/连字符的键原样抄、大小写撞名、数组只看第 0 个
notebook 里眼看一下结构 转代码的 Python true/Infinity 不是 Python 字面量
要贴进 HTML 的代码片段 转义 只对五个字符生效,已转义的文本别再转一遍
把实体还原成字符 「反转义」 只有五个实体,一层要一次交换
自己的 Markdown 笔记 预览 代码块里的井号会变标题
别人给的 Markdown 谁都别用 一个引号就能挂上 onclick

常见问题

我的 1e309 怎么变成 null 了? JSON.parse 给你 Infinity,JSON.stringify 没有对应写法,于是写 null——而页面照样报「格式化成功」。在期望数字的地方扫一眼有没有 null。

为什么输出框里还是上一次的结果,横幅却是红的? 这是这个页面的失败路径:解析失败时它只挂红条,不清空输出、不改「字符数」。所以「输出没变 + 红条」= 刚才那一次没生效。

我输入 null 点格式化,页面一点反应都没有? 因为 JSON.parse('null') 的结果正好是页面表示「解析失败」的哨兵值,于是被当成失败静默返回,而失败提示这次又没机会出现。结果就是没有红条也没有绿条、输出和字符数都不动。把 null 包一层({"a":null})就能正常看到输出。

我改了一个字再点格式化,之前那条错误还在屏幕上,是不是没清掉? 不是。输入框没有监听,横幅只在按钮被按下时收起。红条残留说明你还没重新点过。

按了两次「反转义」,第二次怎么没变化? 按钮每次都从输入框取原文,所以两次结果相同。要去第二层:点「交换」把输出搬到输入,再按一次「反转义」。

代码块里的一行 # comment 为什么变成大标题了? 围栏是在标题、列表、引用之后才被替换的,所以内容已经先被 Markdown 化过一次。这个预览不适合带 #、-、1. 起头的代码。

```python 为什么不高亮? 语言名被正则吃掉但没有使用,产出的只是 <pre><code>,没有 class。要高亮请用真正的渲染器。

![图片](链接) 为什么前面多个感叹号? 这个页面没有图片语法:[文字](URL) 的规则照样命中它,于是变成「一个感叹号 + 一个链接」。

左右两个「字数」为什么一模一样? 因为它们是同一个数:渲染后页面把源码长度同时写给两个标签。右边不是渲染出来的文字数。

这个预览安全吗?我拿它看别人发的 Markdown。 不。escapeHtml 只处理 & < >,不处理引号,所以链接 URL 里的一个 " 就能注入 onclick;实测注入的处理器确实被执行了。自己的笔记无所谓,陌生内容请别在这里渲染。

「字符数」能当字节数用吗? 不能。它是 UTF-16 单元数:中文在线上是 3 字节、这里算 1 个字符,一个 emoji 算 2 个字符。要算线上大小请另找工具。

本指南用到的工具

广告