first commit
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
# JSONML Cookbook
|
||||
|
||||
> 本文档提供 JSONML **结构范例**。正常执行入口是 `+create --content @./相对文件 --doc-format jsonml` 与 `+update --content @./相对文件 --doc-format jsonml`;原子 `doc create/update/block` 仅用于 shortcut 未公开字段的专家路径,使用前读取精确 leaf Schema。
|
||||
> 所有示例均基于真实文档 serialize 输出验证。节点结构详细定义见 [doc-jsonml-schema.md](./doc-jsonml-schema.md)。
|
||||
> 合法节点类型和属性的权威参考为 `wukong/products/jsonml-schema-v2.json`。
|
||||
|
||||
## 决策型文档骨架范例(doc create 用)
|
||||
|
||||
以下是一个"方案对比汇报"的完整 JSONML 文件内容,展示摘要 callout + 彩色表格 + 状态高亮。
|
||||
**可保存到工作目录内的 `./drafts/<name>.json`,再用 `dws doc +create --name "..." --content @./drafts/<name>.json --doc-format jsonml` 创建。**
|
||||
|
||||
```json
|
||||
["root", {},
|
||||
["container", {"subType": "colorBlocks", "metadata": {"bgcolor": "#E8F5E9", "border": "left"}},
|
||||
["p", {}, ["span", {"data-type": "text"},
|
||||
["span", {"data-type": "leaf", "bold": true, "sz": 14, "szUnit": "pt"}, "✅ 推荐方案 A:上线快、依赖已有流程"]
|
||||
]],
|
||||
["p", {}, ["span", {"data-type": "text"},
|
||||
["span", {"data-type": "leaf"}, "主要风险:权限配置需补 | 决策时限:本周五前"]
|
||||
]]
|
||||
],
|
||||
["h2", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "方案对比"]]],
|
||||
["table", {"colsWidth": [120, 200, 200]},
|
||||
["tr", {},
|
||||
["tc", {"fill": "#F5F5F5"}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "维度"]]]],
|
||||
["tc", {"fill": "#E8F5E9"}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "方案 A(推荐)"]]]],
|
||||
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "方案 B"]]]]
|
||||
],
|
||||
["tr", {},
|
||||
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "上线周期"]]]],
|
||||
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "color": "#2E7D32"}, "1 周"]]]],
|
||||
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "3 周"]]]]
|
||||
],
|
||||
["tr", {},
|
||||
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "风险"]]]],
|
||||
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "低"]]]],
|
||||
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "color": "#C62828"}, "高:需新流程审批"]]]]
|
||||
]
|
||||
],
|
||||
["h2", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "下一步"]]],
|
||||
["p", {}, ["span", {"data-type": "text"},
|
||||
["span", {"data-type": "leaf"}, "• "],
|
||||
["span", {"data-type": "leaf", "highlight": "#FFF9C4"}, "待确认"],
|
||||
["span", {"data-type": "leaf"}, " @负责人 完成权限配置"]
|
||||
]]
|
||||
]
|
||||
```
|
||||
|
||||
**设计要点**:
|
||||
- 根节点固定 `"root"`,不是 `"body"`
|
||||
- 摘要用 `container`(callout),`metadata.bgcolor` 选浅绿表示"推荐结论"
|
||||
- 表格用 `table → tr → tc`(无 th/td),表头底色用 `tc` 的 `"fill"` 属性
|
||||
- 关键数据着色用 leaf 的 `"color"`(绿=好 / 红=风险)
|
||||
- 状态标记用 leaf 的 `"highlight"`(黄=待确认、绿=完成、红=阻塞)
|
||||
- uuid 必须显式提供——CLI 不再自动补充
|
||||
|
||||
## ⚠️ JSONML 结构严格约束(生成时必须遵守)
|
||||
|
||||
每个节点是一个 JSON 数组:`[tagName, attributes?, ...children]`
|
||||
|
||||
- **第一个元素**是字符串,表示标签名(如 `"p"`, `"h1"`, `"span"`, `"container"`)
|
||||
- **第二个元素**(可选)是一个 JSON 对象,表示属性(如 `{"uuid": "abc"}`)。如果无属性,可以直接进入子节点
|
||||
- **随后的元素**是子节点,可以是纯字符串(仅限 leaf span 内),也可以是另一个 JSONML 数组
|
||||
- **所有 `[` 必须有对应 `]`,所有 `{` 必须有对应 `}`,数组元素之间用 `,` 分隔,最后一个元素后不加 `,`**
|
||||
|
||||
常见 LLM 生成错误(务必避免):
|
||||
|
||||
| 错误类型 | 示例 | 后果 |
|
||||
|---------|------|------|
|
||||
| 缺少闭合 `]` | `["p", {}, ["span", ...]` | JSON 解析失败 |
|
||||
| 多余逗号 | `["p", {},]` | JSON 解析失败 |
|
||||
| 缺少逗号 | `["p", {} ["span"]]` | JSON 解析失败 |
|
||||
| 引号不匹配 | `["p", {"uuid": "abc}]` | JSON 解析失败 |
|
||||
|
||||
## 文本节点格式(最重要)
|
||||
|
||||
钉钉文档的文本是**三层结构**,不是裸字符串:
|
||||
|
||||
```json
|
||||
["p", {"uuid": "xxx"},
|
||||
["span", {"data-type": "text"},
|
||||
["span", {"data-type": "leaf"}, "文本内容"]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
- **text 容器**:`["span", {"data-type": "text"}, ...leaves]` — 包裹所有文本 leaf
|
||||
- **leaf 节点**:`["span", {"data-type": "leaf", ...格式属性}, "文字"]` — 实际文本,可带 bold/italic 等
|
||||
- 一个 block 节点只有一个 text 容器,但可以有多个 leaf(不同格式的文字片段)
|
||||
|
||||
**简写**:无格式纯文本可以省略格式属性:
|
||||
```json
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "纯文本"]]
|
||||
```
|
||||
|
||||
## 核心规则
|
||||
|
||||
1. **每个 block 节点应有 uuid**:`["tag", {"uuid": "唯一ID"}, ...children]`
|
||||
- insert 时必须提供 uuid(可自行生成任意唯一字符串,后端会自动分配正式 uuid)
|
||||
- update 时 uuid **必须**与 `--block-id` 一致
|
||||
- uuid 必须显式提供,不再自动补充
|
||||
2. **文本必须用 span + leaf 三层结构**,不要直接写裸字符串
|
||||
- ✅ `["p", {"uuid": "x"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "hello"]]]`
|
||||
- ❌ `["p", {"uuid": "x"}, "hello"]` — validator 会报错,请手动包成 ✅ 的形式
|
||||
- ⚠️ `["p", {"uuid": "x"}, ["text", {}, "hello"]]` — `text` 是历史 inline tag,validator 不会报错,但建议改写为 ✅ 形式以与 `dws doc read --content-format jsonml` 的输出保持一致
|
||||
3. **attrs 对象必须存在**(即使为空):`["p", {}, ...]` 不能省略 `{}`
|
||||
|
||||
> **严格模式(缺省)**:CLI 不做结构修复,裸字符串等错误会被 validator 以 `JSONPath + Suggestion` 形式逐条报错。如果输入来自 LLM 且可能有 JSON 语法错误(缺括号/逗号),用 `--fix-jsonml` 启用 JSON 语法修复。
|
||||
|
||||
## 段落 (p)
|
||||
|
||||
```bash
|
||||
# 纯文本段落
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["p", {"uuid": "new1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "这是一段普通文本"]]]'
|
||||
|
||||
# 带格式文本(多个 leaf)
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["p", {"uuid": "new2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "加粗"], ["span", {"data-type": "leaf"}, "普通"], ["span", {"data-type": "leaf", "italic": true}, "斜体"]]]'
|
||||
|
||||
# 多行文本(每行一个 p,同一 p 内的多个 span 不会换行)
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["p", {"uuid": "line1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "第一行标题"]]]' \
|
||||
--element '["p", {"uuid": "line2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "第二行正文内容"]]]'
|
||||
|
||||
# 带链接(link 是与 text 并列的子节点)
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["p", {"uuid": "new3"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "请访问"]], ["a", {"href": "https://example.com"}, "链接文字"]]'
|
||||
```
|
||||
|
||||
**leaf 支持的格式属性**:
|
||||
- `bold: true` — 加粗
|
||||
- `italic: true` — 斜体
|
||||
- `underline: {"value": "single"}` — 下划线(value: `single`/`dash`/`wave`/`double`/`none`,可选 `color`)
|
||||
- `strike: true` — 删除线
|
||||
- `dstrike: true` — 双删除线
|
||||
- `color: "#ff0000"` — 文字颜色(`#rrggbb` 格式)
|
||||
- `highlight: "#ffff00"` — 高亮背景色
|
||||
- `sz: 14` / `szUnit: "pt"` — 字号(szUnit 默认 `"px"`,推荐显式写 `"pt"`)
|
||||
`fonts: {"ascii": "Arial", "eastAsia": "SimHei"}` — 字体(四分区:ascii/hAnsi/cs/eastAsia,值必须使用 font-family 名称,见下方字体表)
|
||||
- `vertAlign: "superscript"` — 上标(`"subscript"` 下标,`"baseline"` 基线)
|
||||
- `spacing: 2` — 字间距(单位 pt)
|
||||
|
||||
**字体名称映射**(`fonts` 字段必须使用 font-family 值,不能写中文名):
|
||||
|
||||
| 用户说法 | font-family 值 | 用户说法 | font-family 值 |
|
||||
|---------|---------------|---------|---------------|
|
||||
| 宋体 | `SimSun` | 黑体 | `SimHei` |
|
||||
| 微软雅黑 | `Microsoft YaHei` | 微软雅黑UI | `Microsoft YaHei UI` |
|
||||
| 仿宋 | `FangSong` | 仿宋_GB2312 | `FangSong_GB2312` |
|
||||
| 楷体 | `KaiTi` | 楷体_GB2312 | `KaiTi_GB2312` |
|
||||
| 等线 | `DengXian` | 新宋体 | `NSimSun` |
|
||||
| 宋体-简 | `SimSun SC` | 宋体-繁 | `SimSun TC` |
|
||||
| 黑体-简 | `Heiti SC` | 黑体-繁 | `Heiti TC` |
|
||||
| 华文宋体 | `STSong` | 华文黑体 | `STHeiti` |
|
||||
| 华文楷体 | `STKaiti` | 华文仿宋 | `STFangsong` |
|
||||
| 华文中宋 | `STZhongsong` | 华文行楷 | `STXingkai` |
|
||||
| 华文隶书 | `STLiti` | 华文新魏 | `STXinwei` |
|
||||
| 华文细黑 | `STXihei` | 华文琥珀 | `STHupo` |
|
||||
| 苹方-简 | `PingFang SC` | 苹方-繁 | `PingFang TC` |
|
||||
| 苹方-港 | `PingFang HK` | 冬青黑-简 | `Hiragino Sans GB` |
|
||||
| 兰亭黑-简 | `Lantinghei SC` | 兰亭黑-繁 | `Lantinghei TC` |
|
||||
| 凌慧体-简 | `LingWai SC` | 幼圆 | `YouYuan` |
|
||||
| 思源黑体 | `Source Han Sans CN` | 思源宋体 | `Source Han Serif CN` |
|
||||
| 思源等宽 | `Source Han Mono SC` | 思源黑体Regular | `Source Han Sans CN Regular` |
|
||||
| 阿里普惠体2.0 | `"Alibaba PuHuiTi 2.0"` | 阿里普惠体3.0 | `"Alibaba PuHuiTi 3.0"` |
|
||||
| 钉钉进步体 | `DingTalk JinBuTi` | Adobe仿宋 | `Adobe 仿宋 Std` |
|
||||
| 方正小标宋_GBK | `FZXiaoBiaoSong-B05` | 方正小标宋简体 | `FZXiaoBiaoSong-B05S` |
|
||||
| 方正黑体 | `FZHei-B01S` | 方正楷体 | `FZKai-Z03S` |
|
||||
| 方正仿宋 | `FZFangSong-Z02S` | 方正仿宋_GBK | `FZFangSong-Z02` |
|
||||
| PMingLiU | `PMingLiU` | — | — |
|
||||
|
||||
**英文字体**(font-family 值即为字体名):
|
||||
`Arial` ・ `Calibri` ・ `Cambria` ・ `Centaur` ・ `Comfortaa` ・ `Comic Sans MS` ・ `Courier New` ・ `Franklin Gothic` ・ `Garamond` ・ `Georgia` ・ `Helvetica` ・ `Impact` ・ `Lora` ・ `Lucida Sans` ・ `Merriweather` ・ `Montserrat` ・ `Nunito` ・ `Oswald` ・ `Playfair Display` ・ `Roboto` ・ `Spectral` ・ `Times New Roman` ・ `Trebuchet MS` ・ `Verdana`
|
||||
|
||||
> **规则**:优先从上表匹配;用户指定的字体不在列表时,使用该字体在操作系统中的真实 font-family 名称(如"更纱黑体" → `Sarasa Gothic SC`)。
|
||||
|
||||
**leaf 组合示例**:
|
||||
|
||||
```json
|
||||
["span", {"data-type": "leaf", "bold": true, "color": "#C62828", "sz": 16, "szUnit": "pt"}, "红色加粗大字"]
|
||||
["span", {"data-type": "leaf", "strike": true, "color": "#9E9E9E"}, "已废弃内容"]
|
||||
["span", {"data-type": "leaf", "vertAlign": "superscript"}, "[1]"]
|
||||
["span", {"data-type": "leaf", "fonts": {"ascii": "Courier New", "eastAsia": "DengXian"}}, "等宽字体"]
|
||||
```
|
||||
|
||||
**段落级排版属性**(写在 p/h1-h6 的 attrs 上):
|
||||
- `jc: "center"` — 对齐(`left`/`center`/`right`/`both`/`justify`)
|
||||
- `spacing: {"line": 1.5, "lineRule": "auto"}` — 行距(lineRule=auto 时 line 为倍数:1=单倍、1.5=1.5倍、2=双倍)
|
||||
- `spacing: {"before": 12, "after": 8}` — 段前/段后间距(单位 pt)
|
||||
- `ind: {"firstLine": 32}` — 首行缩进(≈ 2 中文字符)
|
||||
- `ind: {"left": 96}` — 左缩进
|
||||
|
||||
**段落排版示例**:
|
||||
|
||||
```json
|
||||
["p", {"uuid": "p1", "jc": "center"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "居中段落"]]]
|
||||
["p", {"uuid": "p2", "spacing": {"line": 1.5, "lineRule": "auto"}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "1.5倍行距"]]]
|
||||
["p", {"uuid": "p3", "spacing": {"line": 2, "lineRule": "auto", "before": 12, "after": 8}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "双倍行距+段前后间距"]]]
|
||||
```
|
||||
|
||||
## 标题 (h1-h6)
|
||||
|
||||
```bash
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["h1", {"uuid": "new4"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "一级标题"]]]'
|
||||
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["h2", {"uuid": "new5"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "二级标题"]]]'
|
||||
|
||||
# 更新已有标题
|
||||
dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --content-format jsonml \
|
||||
--element '["h2", {"uuid": "<BLOCK_ID>"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "修改后的标题"]]]'
|
||||
```
|
||||
|
||||
## 列表 (list)
|
||||
|
||||
列表在 JSONML 中是 **带 `list` 属性的 `p` 节点**,不是独立 tag。
|
||||
|
||||
```bash
|
||||
# 无序列表项
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["p", {"uuid": "li1", "list": {"listId": "mylist1", "level": 0, "isOrdered": false}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "无序列表第一项"]]]'
|
||||
|
||||
# 有序列表项(仅第一项设 start,后续项不设,系统自动递增)
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["p", {"uuid": "li2", "list": {"listId": "mylist2", "level": 0, "isOrdered": true, "start": 1}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "有序列表第一项"]]]'
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["p", {"uuid": "li2b", "list": {"listId": "mylist2", "level": 0, "isOrdered": true}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "有序列表第二项"]]]'
|
||||
|
||||
# 缩进子项(level: 1)
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["p", {"uuid": "li3", "list": {"listId": "mylist2", "level": 1, "isOrdered": true}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "子列表项"]]]'
|
||||
|
||||
# 待办列表(checkbox)
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["p", {"uuid": "li4", "list": {"listId": "todo1", "level": 0, "isOrdered": false, "isTaskList": true, "isChecked": false}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "待办事项"]]]'
|
||||
```
|
||||
|
||||
## 引用 (blockquote)
|
||||
|
||||
引用是 **带 `quote` 属性的 `p` 节点**。
|
||||
|
||||
```bash
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["p", {"uuid": "q1", "quote": true}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "这是一段引用文字"]]]'
|
||||
```
|
||||
|
||||
## 高亮块 / Callout (container)
|
||||
|
||||
```bash
|
||||
# 蓝色高亮块
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["container", {"uuid": "co1", "subType": "colorBlocks", "metadata": {"bgcolor": "#E8F2FE", "border": "#B3D4FC"}}, ["p", {"uuid": "co1p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "这是一段提示内容"]]]]'
|
||||
|
||||
# 黄色警告块(多段落)
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["container", {"uuid": "co2", "subType": "colorBlocks", "metadata": {"bgcolor": "#FFF2CC", "border": "#FFE599"}}, ["p", {"uuid": "co2p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "⚠️ 注意事项"]]], ["p", {"uuid": "co2p2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "请仔细阅读以下内容"]]]]'
|
||||
```
|
||||
|
||||
**常用颜色预设**:
|
||||
|
||||
| 含义 | bgcolor | border |
|
||||
|------|---------|--------|
|
||||
| 信息(蓝) | `#E8F2FE` | `#B3D4FC` |
|
||||
| 成功(绿) | `#E6F7E6` | `#B7EB8F` |
|
||||
| 警告(黄) | `#FFF2CC` | `#FFE599` |
|
||||
| 危险(红) | `#FFF1F0` | `#FFA39E` |
|
||||
| 紫色 | `#F3E8FF` | `#D3ADF7` |
|
||||
|
||||
## 代码块 (code)
|
||||
|
||||
代码内容存在 attrs.code 中,不需要 text/leaf 子节点。
|
||||
|
||||
```bash
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["code", {"uuid": "cd1", "syntax": "javascript", "code": "function hello() {\n return \"world\";\n}"}]'
|
||||
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["code", {"uuid": "cd2", "syntax": "python", "code": "print(\"hello\")", "showLineNumber": true, "theme": "dracula"}]'
|
||||
```
|
||||
|
||||
## 分割线 (hr)
|
||||
|
||||
```bash
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["hr", {"uuid": "hr1"}]'
|
||||
```
|
||||
|
||||
## 表格 (table)
|
||||
|
||||
> colsWidth 单位为 **pt**(页宽约 650pt)。如配合 `tblW: {"type": "pct"}` 则为百分比权重。
|
||||
|
||||
```bash
|
||||
# 2行2列表格(各列 200pt)
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["table", {"uuid": "tb1", "colsWidth": [200, 200]}, ["tr", {"uuid": "tr1"}, ["tc", {"uuid": "tc1", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "标题A"]]]], ["tc", {"uuid": "tc2", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "标题B"]]]]], ["tr", {"uuid": "tr2"}, ["tc", {"uuid": "tc3", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp3"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "数据1"]]]], ["tc", {"uuid": "tc4", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp4"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "数据2"]]]]]]]'
|
||||
```
|
||||
|
||||
> 表格较复杂时建议写入文件后用 `--element "$(cat table.json)"` 传入。
|
||||
|
||||
## 图片 (img)
|
||||
|
||||
> `img` 是 inline 元素,必须包裹在 `p` 段落中才能作为 block 插入。
|
||||
|
||||
```bash
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["p", {"uuid": "p-img1"}, ["img", {"uuid": "img1", "src": "https://example.com/photo.png", "width": 400, "height": 300}], ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, ""]]]'
|
||||
```
|
||||
|
||||
## 分栏布局 (columns)
|
||||
|
||||
分栏复用 table tag,通过 `sr: true` 区分。分栏的 `tc` 可设置 `fill`(背景色)和 `border`(边框)属性提升视觉效果。
|
||||
|
||||
```bash
|
||||
# 两栏布局(带背景色)
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["table", {"uuid": "col1", "sr": true, "colsWidth": [300, 300]}, ["tr", {"uuid": "coltr"}, ["tc", {"uuid": "coltc1", "fill": "#EEF6FF", "vAlign": "top"}, ["p", {"uuid": "colp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "左栏内容"]]]], ["tc", {"uuid": "coltc2", "fill": "#FFF3E0", "vAlign": "top"}, ["p", {"uuid": "colp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "右栏内容"]]]]]]'
|
||||
```
|
||||
|
||||
**分栏视觉属性**:
|
||||
- `fill` — 单元格背景色(推荐淡色,如 `#EEF6FF` / `#FFF8E1` / `#F3E5F5`)
|
||||
- `border` — 边框配置(可选)
|
||||
- 分栏建议始终设置 `fill` 背景色,纯白底分栏视觉上与普通段落无异,读者无法感知分栏结构
|
||||
|
||||
## 嵌入块 (embed)
|
||||
|
||||
通用文件/iframe 嵌入。`embed` 是 void 块,仅含 attrs,无子节点。
|
||||
|
||||
```bash
|
||||
# 嵌入文件预览
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["embed", {"uuid": "em1", "name": "design.pdf", "type": "pdf", "src": "https://example.com/design.pdf", "size": 524288, "viewType": "preview", "previewSize": {"height": 600}}]'
|
||||
```
|
||||
|
||||
**关键 attrs**:
|
||||
- `src`(**必填**)— 资源 URL
|
||||
- `type` — `pdf` / `xlsx` / `html` 等
|
||||
- `name` — 展示名
|
||||
- `previewSize.height` — 预览高度(px)
|
||||
|
||||
## 在线视频 (onlineVideo)
|
||||
|
||||
外链视频(B 站 / 优酷 / 自定义 mp4 等)。
|
||||
|
||||
```bash
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["onlineVideo", {"uuid": "ov1", "src": "https://player.bilibili.com/player.html?aid=12345", "type": "bilibili", "poster": "https://example.com/poster.jpg"}]'
|
||||
```
|
||||
|
||||
**关键 attrs**:
|
||||
- `src`(**必填**)— 视频播放页或 mp4 URL
|
||||
- `type` — 平台标识(`bilibili` / `youku` / `mp4` 等)
|
||||
- `poster` — 封面图 URL
|
||||
|
||||
## 卡片 (card)
|
||||
|
||||
群名片 / 应用卡片等富交互卡片。`cardType` 决定渲染形态,`metadata` 内容随类型变化。
|
||||
|
||||
```bash
|
||||
# 群名片
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml \
|
||||
--element '["card", {"uuid": "cd1", "cardType": "groupChatCard", "metadata": {"id": "63953109506", "name": "测试组", "inviteUrl": "https://qr.dingtalk.com/...", "expires": 1810865989}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, ""]]]'
|
||||
```
|
||||
|
||||
> 注意:服务端 serialize 出来的 card 通常带一个空的 span/leaf 占位子节点,建议保留以避免反序列化差异。
|
||||
|
||||
## 目录 (toc)
|
||||
|
||||
目录块 attrs 上必带 4 个字段:`title` / `mode` / `styles` / `content`。如果不知道怎么填,**最简方式**是先在 web 端插入一个 toc,然后 `block list --content-format jsonml` 把现成结构拿下来改。
|
||||
|
||||
```bash
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml --element '
|
||||
["toc", {
|
||||
"uuid": "toc1",
|
||||
"title": "目录",
|
||||
"mode": "outline",
|
||||
"styles": {
|
||||
"global": {"maxLevel": 5, "bgColor": "#F0EBF7", "css": {}},
|
||||
"title": {"font": "DingTalk JinBuTi", "color": "#6940A5", "numbering": true, "css": {"fontWeight": "normal"}},
|
||||
"item": {"symbol": "disc", "css": {}}
|
||||
},
|
||||
"content": []
|
||||
}]'
|
||||
```
|
||||
|
||||
**关键 attrs**:
|
||||
- `mode` — `outline`(大纲)/ `column`(分栏)
|
||||
- `styles.global.maxLevel` — 最大显示层级
|
||||
- `content` — 目录条目数组;**留空数组即可**,服务端会基于文档 heading 自动重建
|
||||
|
||||
## 引用块 (refblock)
|
||||
|
||||
引用另一文档的内容片段。`refblock` 像容器一样包子节点。
|
||||
|
||||
```bash
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml --element '
|
||||
["refblock", {"uuid": "rb1", "docKey": "OTHER_DOC_NODE_ID", "refblockUUID": "BLOCK_UUID_IN_OTHER_DOC"},
|
||||
["p", {"uuid": "rb1p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "(引用预览内容,服务端会回填)"]]]
|
||||
]'
|
||||
```
|
||||
|
||||
**关键 attrs**:
|
||||
- `docKey` — 被引用文档的 nodeId
|
||||
- `refblockUUID` — 被引用块的 uuid
|
||||
|
||||
> 引用块的子节点是「快照」,真实内容由服务端按 docKey/refblockUUID 拉取覆写。
|
||||
|
||||
## 表格单元格嵌套块 (tableCell with nested blocks)
|
||||
|
||||
`tc` 的子节点是**块级节点**(不仅是 `p`)。可以塞多个段落、列表、代码块、甚至嵌套表格。
|
||||
|
||||
```bash
|
||||
# 单元格内含多段落 + 代码块
|
||||
dws doc block insert --node <DOC_ID> --content-format jsonml --element '
|
||||
["table", {"uuid": "tbn1", "colsWidth": [400]},
|
||||
["tr", {"uuid": "tbn1r1"},
|
||||
["tc", {"uuid": "tbn1c1", "colSpan": 1, "rowSpan": 1},
|
||||
["p", {"uuid": "tbn1p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "标题段落"]]],
|
||||
["p", {"uuid": "tbn1p2", "list": {"listId": "tbn1list", "level": 0, "isOrdered": false}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "列表项 1"]]],
|
||||
["p", {"uuid": "tbn1p3", "list": {"listId": "tbn1list", "level": 0, "isOrdered": false}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "列表项 2"]]],
|
||||
["code", {"uuid": "tbn1code", "syntax": "bash", "code": "echo hello"}]
|
||||
]
|
||||
]
|
||||
]'
|
||||
```
|
||||
|
||||
**规则**:
|
||||
- `tc` 子节点 **必须是块节点数组**,不能直接放 `span` 或裸字符串
|
||||
- 至少包含一个 `["p", {...}, ...]`(即使空也要),否则单元格无法渲染光标
|
||||
- 单元格可嵌套 `table`,但嵌套时务必保证内层每个 `tc` 也满足上述规则
|
||||
|
||||
## Update 操作注意事项
|
||||
|
||||
1. **uuid 必须与 --block-id 一致**
|
||||
2. Update 是**整块替换**,不是 patch — 需提供完整节点结构
|
||||
3. 推荐流程:先 `block list --content-format jsonml --block-id <ID>` 获取当前结构,修改后写回
|
||||
|
||||
```bash
|
||||
# 典型 update 流程
|
||||
# 1. 获取当前结构
|
||||
dws doc block list --node <DOC_ID> --content-format jsonml --block-id <BLOCK_ID>
|
||||
|
||||
# 2. 修改后写回(uuid 不变)
|
||||
dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --content-format jsonml \
|
||||
--element '["p", {"uuid": "<BLOCK_ID>"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "修改后的内容"]]]'
|
||||
```
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 错误写法 | 问题 | 正确写法 |
|
||||
|---------|------|---------|
|
||||
| `["p", {}, "文字"]` | 裸字符串。validator 会报 `段落子节点不能是裸字符串`,请手动包成右侧形式 | `["p", {}, ["span", {"data-type":"text"}, ["span", {"data-type":"leaf"}, "文字"]]]` |
|
||||
| `["p", {}, ["text", {}, "文字"]]` | `text` 是历史 inline tag,validator 不报错但服务端实际渲染的 canonical 形式是 span/leaf;为与 `doc read` 输出一致,建议改写 | 同上,用 span + data-type |
|
||||
| `["callout", {}, ...]` | 不存在 callout tag | `["container", {"subType": "colorBlocks", ...}, ...]` |
|
||||
| `["list", {}, ...]` | 不存在 list tag | `["p", {"list": {...}}, ...]` |
|
||||
| `["blockquote", {}, ...]` | 不存在 blockquote tag | `["p", {"quote": true}, ...]` |
|
||||
| `["ul", {}, ["li", ...]]` | 不存在 ul/li tag | 多个 `["p", {"list": {...}}, ...]` |
|
||||
|
||||
## 快捷模板
|
||||
|
||||
为方便使用,以下是最常用节点的最小完整模板:
|
||||
|
||||
```
|
||||
纯文本段落: ["p", {"uuid":"U"}, ["span", {"data-type":"text"}, ["span", {"data-type":"leaf"}, "TEXT"]]]
|
||||
标题: ["h2", {"uuid":"U"}, ["span", {"data-type":"text"}, ["span", {"data-type":"leaf"}, "TITLE"]]]
|
||||
代码块: ["code", {"uuid":"U", "syntax":"LANG", "code":"CODE"}]
|
||||
分割线: ["hr", {"uuid":"U"}]
|
||||
```
|
||||
@@ -0,0 +1,526 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "DingTalk Document JSONML Body Schema",
|
||||
"description": "钉钉文档 body JSONML 的结构校验 schema",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/blockNode" },
|
||||
"definitions": {
|
||||
"blockNode": {
|
||||
"oneOf": [
|
||||
{ "$ref": "#/definitions/paragraph" },
|
||||
{ "$ref": "#/definitions/heading" },
|
||||
{ "$ref": "#/definitions/hr" },
|
||||
{ "$ref": "#/definitions/table" },
|
||||
{ "$ref": "#/definitions/code" },
|
||||
{ "$ref": "#/definitions/container" },
|
||||
{ "$ref": "#/definitions/embed" },
|
||||
{ "$ref": "#/definitions/onlineVideo" },
|
||||
{ "$ref": "#/definitions/card" },
|
||||
{ "$ref": "#/definitions/toc" },
|
||||
{ "$ref": "#/definitions/refblock" }
|
||||
]
|
||||
},
|
||||
"inlineContent": {
|
||||
"oneOf": [
|
||||
{ "type": "string" },
|
||||
{ "$ref": "#/definitions/textNode" },
|
||||
{ "$ref": "#/definitions/link" },
|
||||
{ "$ref": "#/definitions/image" },
|
||||
{ "$ref": "#/definitions/mention" },
|
||||
{ "$ref": "#/definitions/formula" },
|
||||
{ "$ref": "#/definitions/sticker" },
|
||||
{ "$ref": "#/definitions/inlineCode" },
|
||||
{ "$ref": "#/definitions/br" }
|
||||
]
|
||||
},
|
||||
"textNode": {
|
||||
"type": "array",
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
"items": [
|
||||
{ "type": "string", "const": "text" },
|
||||
{ "$ref": "#/definitions/textMarks" },
|
||||
{ "type": "string" }
|
||||
]
|
||||
},
|
||||
"textMarks": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bold": { "type": "boolean" },
|
||||
"italic": { "type": "boolean", "const": true },
|
||||
"strike": { "type": "boolean" },
|
||||
"dstrike": { "type": "boolean" },
|
||||
"underline": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": { "type": "string", "enum": ["single", "dash", "wave", "double", "none"] },
|
||||
"color": { "type": "string" }
|
||||
},
|
||||
"required": ["value"]
|
||||
},
|
||||
"color": { "type": "string" },
|
||||
"highlight": { "type": "string" },
|
||||
"shd": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"val": { "type": "string" },
|
||||
"color": { "type": "string" },
|
||||
"fill": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"sz": { "type": "number" },
|
||||
"szUnit": { "type": "string", "enum": ["px", "pt"], "default": "px" },
|
||||
"fonts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ascii": { "type": "string" },
|
||||
"hAnsi": { "type": "string" },
|
||||
"cs": { "type": "string" },
|
||||
"eastAsia": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"vertAlign": { "type": "string", "enum": ["superscript", "subscript", "baseline"] },
|
||||
"spacing": { "type": "number", "description": "字间距,单位 pt" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"paragraphAttrs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jc": { "type": "string", "enum": ["left", "center", "right", "both", "distribute", "justify"] },
|
||||
"ind": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"left": { "type": "number" },
|
||||
"start": { "type": "number" },
|
||||
"leftChars": { "type": "number" },
|
||||
"right": { "type": "number" },
|
||||
"end": { "type": "number" },
|
||||
"rightChars": { "type": "number" },
|
||||
"hanging": { "type": "number" },
|
||||
"hangingChars": { "type": "number" },
|
||||
"firstLine": { "type": "number" },
|
||||
"firstLineChars": { "type": "number" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"spacing": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"line": { "type": "number" },
|
||||
"before": { "type": "number" },
|
||||
"beforeLines": { "type": "number" },
|
||||
"beforeAutospacing": { "type": "boolean" },
|
||||
"after": { "type": "number" },
|
||||
"afterLines": { "type": "number" },
|
||||
"afterAutospacing": { "type": "boolean" },
|
||||
"lineRule": { "type": "string", "enum": ["atLeast", "auto", "exact"] }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"shd": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"val": { "type": "string" },
|
||||
"color": { "type": "string" },
|
||||
"fill": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"blockquote": { "type": "boolean" },
|
||||
"list": { "$ref": "#/definitions/listProperties" },
|
||||
"refs": { "type": "array", "items": { "type": "string" } }
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"listProperties": {
|
||||
"type": "object",
|
||||
"required": ["listId", "level"],
|
||||
"properties": {
|
||||
"listId": { "type": "string" },
|
||||
"level": { "type": "integer", "minimum": 0 },
|
||||
"isOrdered": { "type": "boolean", "default": false },
|
||||
"isTaskList": { "type": "boolean", "default": false },
|
||||
"isChecked": { "type": "boolean" },
|
||||
"isCanceled": { "type": "boolean" },
|
||||
"start": { "type": "integer", "minimum": 1 },
|
||||
"listStyleType": { "type": "string" },
|
||||
"hideSymbol": { "type": "boolean" },
|
||||
"listStyle": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"format": { "type": "string", "enum": ["bullet", "decimal", "decimalZero", "lowerLetter", "lowerRoman", "upperLetter", "upperRoman", "chineseCountingThousand"] },
|
||||
"text": { "type": "string" },
|
||||
"align": { "type": "string", "enum": ["left", "start", "end", "center", "both", "right", "distribute"] }
|
||||
},
|
||||
"required": ["format", "text", "align"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"paragraph": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "p" },
|
||||
{ "$ref": "#/definitions/paragraphAttrs" }
|
||||
],
|
||||
"additionalItems": { "$ref": "#/definitions/inlineContent" }
|
||||
},
|
||||
"heading": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "enum": ["h1", "h2", "h3", "h4", "h5", "h6"] },
|
||||
{ "$ref": "#/definitions/paragraphAttrs" }
|
||||
],
|
||||
"additionalItems": { "$ref": "#/definitions/inlineContent" }
|
||||
},
|
||||
"hr": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "hr" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string", "enum": ["single", "dotted", "dashed", "double", "wave", "doubleWave", "dotDash", "dotDotDash", "custom", "thickThinSmallGap", "thinThickThinMediumGap", "dashStroked", "dashDotStroked", "widthDoubleWave", "widthWave", "roundDot"] },
|
||||
"sz": { "type": "number", "default": 1 },
|
||||
"color": { "type": "string" },
|
||||
"width": { "oneOf": [{ "type": "number" }, { "type": "string" }] }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"table": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "table" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"colsWidth": { "type": "array", "items": { "type": "number" } },
|
||||
"sr": { "type": "boolean" },
|
||||
"jc": { "type": "string" },
|
||||
"spacing": { "type": "number" }
|
||||
}
|
||||
}
|
||||
],
|
||||
"additionalItems": { "$ref": "#/definitions/tableRow" }
|
||||
},
|
||||
"tableRow": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "tr" },
|
||||
{ "type": "object" }
|
||||
],
|
||||
"additionalItems": { "$ref": "#/definitions/tableCell" }
|
||||
},
|
||||
"tableCell": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "tc" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"colSpan": { "type": "integer", "minimum": 1, "default": 1 },
|
||||
"rowSpan": { "type": "integer", "minimum": 1, "default": 1 },
|
||||
"fill": { "type": "string" },
|
||||
"vAlign": { "type": "string", "enum": ["top", "middle", "bottom"], "default": "middle" }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
],
|
||||
"additionalItems": { "$ref": "#/definitions/blockNode" }
|
||||
},
|
||||
"code": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "code" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "string", "default": "" },
|
||||
"syntax": { "type": "string", "default": "plaintext" },
|
||||
"theme": { "type": "string", "enum": ["default", "light", "dracula", "github", "cobalt", "atomOneDark", "oneLightPro", "nightOwl", "githubDark", "realDracula"], "default": "default" },
|
||||
"wrap": { "type": "boolean", "default": true },
|
||||
"showLineNumber": { "type": "boolean", "default": true },
|
||||
"title": { "type": "string", "maxLength": 1000 },
|
||||
"fold": { "type": "boolean", "default": false }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"container": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "container" },
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["subType"],
|
||||
"properties": {
|
||||
"subType": { "type": "string" },
|
||||
"metadata": { "type": "object" }
|
||||
}
|
||||
}
|
||||
],
|
||||
"additionalItems": { "$ref": "#/definitions/blockNode" }
|
||||
},
|
||||
"embed": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "embed" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"type": { "type": "string" },
|
||||
"src": { "type": "string" },
|
||||
"size": { "type": "number" },
|
||||
"viewType": { "type": "string" },
|
||||
"previewSize": { "type": "object", "properties": { "height": { "type": "number" } } }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"onlineVideo": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "onlineVideo" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"src": { "type": "string" },
|
||||
"type": { "type": "string" },
|
||||
"poster": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"card": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "card" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cardType": { "type": "string" },
|
||||
"metadata": { "type": "object" },
|
||||
"height": { "type": "number" }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"toc": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "toc" },
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["title", "mode", "styles", "content"],
|
||||
"properties": {
|
||||
"title": { "type": "string" },
|
||||
"mode": { "type": "string", "enum": ["outline", "column"] },
|
||||
"styles": {
|
||||
"type": "object",
|
||||
"required": ["global", "title", "item"],
|
||||
"properties": {
|
||||
"global": {
|
||||
"type": "object",
|
||||
"required": ["maxLevel", "bgColor"],
|
||||
"properties": {
|
||||
"maxLevel": { "type": "integer", "minimum": 1 },
|
||||
"bgColor": { "type": "string" },
|
||||
"css": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"font": { "type": "string" },
|
||||
"color": { "type": "string" },
|
||||
"numbering": { "type": "boolean" },
|
||||
"css": { "type": "object" }
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"symbol": { "type": "string", "enum": ["disc", "none"] },
|
||||
"css": { "type": "object" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/tocItem" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tocItem": {
|
||||
"type": "object",
|
||||
"required": ["uuid", "anchorId", "level", "children"],
|
||||
"properties": {
|
||||
"uuid": { "type": "string" },
|
||||
"anchorId": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
|
||||
"level": { "type": "integer" },
|
||||
"children": { "type": "array", "items": { "$ref": "#/definitions/tocItem" } },
|
||||
"text": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"refblock": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "refblock" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"docKey": { "type": "string" },
|
||||
"refblockUUID": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
],
|
||||
"additionalItems": { "$ref": "#/definitions/blockNode" }
|
||||
},
|
||||
"link": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "a" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"href": { "type": "string" },
|
||||
"cardInfo": { "type": "object" },
|
||||
"metadata": { "type": "object" }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
],
|
||||
"additionalItems": { "$ref": "#/definitions/inlineContent" }
|
||||
},
|
||||
"image": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "img" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"src": { "type": "string" },
|
||||
"width": { "type": "number" },
|
||||
"height": { "type": "number" },
|
||||
"rectClip": { "type": "object" },
|
||||
"rotation": { "type": "number" },
|
||||
"radius": { "type": "number" },
|
||||
"shadow": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"mention": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "span" },
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["data-type"],
|
||||
"properties": {
|
||||
"data-type": { "type": "string", "const": "mention" },
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"login": { "type": "string" },
|
||||
"metadata": { "type": "object" }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
],
|
||||
"additionalItems": { "type": "string" }
|
||||
},
|
||||
"formula": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "tag" },
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["tagType", "metadata"],
|
||||
"properties": {
|
||||
"tagType": { "type": "string", "const": "formula" },
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"required": ["formula"],
|
||||
"properties": {
|
||||
"formula": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"sticker": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "span" },
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["data-type"],
|
||||
"properties": {
|
||||
"data-type": { "type": "string", "const": "emoji" },
|
||||
"code": { "type": "string" },
|
||||
"newCode": { "type": "object" }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"inlineCode": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": [
|
||||
{ "type": "string", "const": "inlineCode" },
|
||||
{ "type": "object", "properties": { "bgColor": { "type": "string" } }, "additionalProperties": true }
|
||||
],
|
||||
"additionalItems": { "$ref": "#/definitions/inlineContent" }
|
||||
},
|
||||
"br": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 3,
|
||||
"items": [
|
||||
{ "type": "string", "const": "br" },
|
||||
{ "type": "object" },
|
||||
{ "type": "string" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,711 @@
|
||||
# 文档 JSONML 节点结构参考
|
||||
|
||||
> **权威定义**:合法节点类型、允许的子节点和属性约束以 `wukong/products/jsonml-schema-v2.json` 为准。本文为可读版摘要,若与 schema-v2.json 冲突以后者为准。
|
||||
|
||||
本文档定义钉钉文档 body JSONML 中所有节点类型的结构,供 agent 编辑文档时参考。
|
||||
**写法范例**见 [doc-jsonml-cookbook.md](./doc-jsonml-cookbook.md);本文聚焦字段定义、枚举与约束。
|
||||
|
||||
## 格式说明
|
||||
|
||||
JSONML 是文档内容树的序列化格式:
|
||||
|
||||
```
|
||||
[tag, attrs?, ...children]
|
||||
```
|
||||
|
||||
- `tag` — 字符串,节点类型标识
|
||||
- `attrs` — 可选对象,节点属性(写入时**强烈建议**始终传 `{}` 而非省略)
|
||||
- `children` — 子节点数组;可以是嵌套节点或(仅 inline 上下文中)字符串
|
||||
|
||||
文档 body 是一个以 `"root"` 为根的 JSONML 节点,`dws doc read --content-format jsonml` 返回此格式:
|
||||
|
||||
```json
|
||||
["root", {"sectPr": {"pgSz": {"w": 11906, "h": 16838}}},
|
||||
["p", {"uuid": "p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "第一段"]]],
|
||||
["p", {"uuid": "p2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "第二段"]]]
|
||||
]
|
||||
```
|
||||
|
||||
- 第一个元素固定为 `"root"`
|
||||
- 第二个元素为文档级属性对象(如 `sectPr` 页面设置),可选
|
||||
- 后续元素为块级节点(每个 block 节点应带 `uuid`)
|
||||
|
||||
全量覆写(overwrite)时,CLI 要求 body 必须以 `["root", ...]` 为根节点:
|
||||
|
||||
1. `["root", {sectPr}, ...blocks]` — 服务端 canonical 形式,`doc read` 输出
|
||||
2. `["root", {}, ...blocks]` — 无页面设置时用空 attrs
|
||||
|
||||
## CLI 行为概览(validator)
|
||||
|
||||
写入端(`doc create/update`、`block insert/update`)走 **validate** 一步,不做结构修复:
|
||||
|
||||
| 行为 | 缺省 | `--fix-jsonml` |
|
||||
|------|------|----------------|
|
||||
| JSON 语法修复(括号/逗号补全) | ✗ | ✓(打印 `[FIX]`) |
|
||||
| validator 阻断(HasErrors → 拒绝发送) | ✓ | ✓ |
|
||||
| validator 警告(warnings → 仅 stderr) | ✓ | ✓ |
|
||||
| root 校验(仅 doc create/update) | ✓ | ✓ |
|
||||
|
||||
> `doc create/update` 要求 body 必须以 `["root", {attrs?}, ...blocks]` 为根节点。缺少 root 会报错而非自动包装。`doc block insert/update` 不要求 root。
|
||||
|
||||
报错格式(面向 agent):
|
||||
|
||||
```
|
||||
$[2][2]: paragraph child must be span wrapper, got raw string.
|
||||
Suggestion: ["span",{"data-type":"text"},["span",{"data-type":"leaf"},"<your text>"]]
|
||||
```
|
||||
|
||||
`$` 表示输入根,`[i]` 是数组下标,`.attrs.k` 是属性名。
|
||||
|
||||
## 文本节点(Text)
|
||||
|
||||
**Canonical 形式**(服务端 serialize 输出、`doc read --content-format jsonml` 返回的就是这个):
|
||||
|
||||
```json
|
||||
["span", {"data-type": "text"},
|
||||
["span", {"data-type": "leaf", "bold": true}, "加粗文本"],
|
||||
["span", {"data-type": "leaf"}, "普通文本"]
|
||||
]
|
||||
```
|
||||
|
||||
- **text 容器**:`["span", {"data-type": "text"}, ...leaves]` — 包裹所有 leaf
|
||||
- **leaf 节点**:`["span", {"data-type": "leaf", ...marks}, "<文本>"]` — 实际承载文字与样式
|
||||
- 一个 block 通常只有一个 text 容器,可以含多个 leaf(不同样式片段)
|
||||
- text 容器与 `["a", ...]`、`["img", ...]`、`["tag", ...]` 等其他 inline 节点**并列**作为 block 的子节点
|
||||
|
||||
### 文本样式属性(Marks,写在 leaf 的 attrs 上)
|
||||
|
||||
所有 marks 均为 optional,按需组合。
|
||||
|
||||
| 属性 | 类型 | 格式/枚举 | 说明 |
|
||||
|------|------|-----------|------|
|
||||
| `bold` | `boolean` | `true` / `false` | 加粗。`false` 可反向取消继承 |
|
||||
| `italic` | `boolean` | `true` | 斜体 |
|
||||
| `strike` | `boolean` | `true` / `false` | 单删除线 |
|
||||
| `dstrike` | `boolean` | `true` / `false` | 双删除线(独立于 strike) |
|
||||
| `underline` | `object` | `{value, color?}` | 下划线。value: `"single"` \| `"dash"` \| `"wave"` \| `"double"` \| `"none"` |
|
||||
| `color` | `string` | `"#rrggbb"` | 文字颜色 |
|
||||
| `highlight` | `string` | CSS 颜色 | 文字高亮背景色 |
|
||||
| `shd` | `object` | `{val?, color?, fill?}` | OOXML 底纹(Word 导入保留) |
|
||||
| `sz` | `number` | 数值 | 字号,配合 `szUnit` |
|
||||
| `szUnit` | `string` | `"px"` \| `"pt"` | 字号单位,默认 `"px"` |
|
||||
| `fonts` | `object` | `{ascii, hAnsi, cs, eastAsia}` | OOXML 四分区字体,值为 font-family 名称(如 `SimHei`),不能写中文名 |
|
||||
| `vertAlign` | `string` | `"superscript"` \| `"subscript"` \| `"baseline"` | 上标/下标/基线 |
|
||||
| `spacing` | `number` | 数值(pt) | 字间距 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
["span", {"data-type": "text"},
|
||||
["span", {"data-type": "leaf", "bold": true, "italic": true, "color": "#1a73e8"}, "加粗斜体蓝字"]
|
||||
]
|
||||
["span", {"data-type": "text"},
|
||||
["span", {"data-type": "leaf", "underline": {"value": "single", "color": "#ff0000"}, "sz": 14, "szUnit": "px"}, "红色下划线"]
|
||||
]
|
||||
["span", {"data-type": "text"},
|
||||
["span", {"data-type": "leaf", "strike": true, "color": "#9E9E9E"}, "删除线灰字"]
|
||||
]
|
||||
["span", {"data-type": "text"},
|
||||
["span", {"data-type": "leaf", "fonts": {"ascii": "Arial", "eastAsia": "SimSun"}, "sz": 12, "szUnit": "pt"}, "指定字体"]
|
||||
]
|
||||
["span", {"data-type": "text"},
|
||||
["span", {"data-type": "leaf"}, "H"],
|
||||
["span", {"data-type": "leaf", "vertAlign": "subscript"}, "2"],
|
||||
["span", {"data-type": "leaf"}, "O"]
|
||||
]
|
||||
```
|
||||
|
||||
### 历史/兼容形式
|
||||
|
||||
| 写法 | validator | 服务端 | 建议 |
|
||||
|------|-----------|--------|------|
|
||||
| `["span", {"data-type":"text"}, ["span", {"data-type":"leaf"}, "x"]]` | ✓ canonical | ✓ | ✅ 新内容首选 |
|
||||
| `["text", {marks}, "x"]` | ✓(`text` 在 inline 白名单中) | ✓(兼容) | ⚠️ 历史 inline tag。`doc read` 不会输出这种形式;如需复制粘贴回写、保持与现有内容一致,建议改写为 canonical |
|
||||
| `"raw string"` 作为 block 子节点 | ✗ 报错 `段落子节点不能是裸字符串` | — | 不要直接写。validator 会报错,请手动包成 canonical 形式 |
|
||||
|
||||
> Marks 表的属性集对 canonical 的 leaf 和 legacy 的 text 都适用;差别仅在承载位置(leaf 的 attrs vs text 的 attrs)。
|
||||
|
||||
---
|
||||
|
||||
## 块级节点
|
||||
|
||||
所有 block 节点的 tag 白名单(validator `validBlockTags`):
|
||||
`p` / `h1` / `h2` / `h3` / `h4` / `h5` / `h6` / `hr` / `table` / `code` / `container` / `embed` / `onlineVideo` / `card` / `toc` / `refblock` / `cangjie-voidblock` / `cangjie-container`
|
||||
|
||||
未在白名单的 tag 会触发 `未知的块级 tag` 警告,并给出基于编辑距离 (Levenshtein ≤2) 的最接近建议(如 `"containr"` → `did you mean "container"?`)。
|
||||
|
||||
### paragraph(段落)
|
||||
|
||||
- **tag**: `"p"`
|
||||
- **attrs**(全部 optional):
|
||||
- `jc?: "left" | "center" | "right" | "both" | "distribute" | "justify"` — 对齐
|
||||
- `ind?` — 缩进
|
||||
- `left?: number`, `right?: number`, `firstLine?: number`, `firstLineChars?: number`, `hanging?: number`
|
||||
- `spacing?` — 行间距(`lineRule=auto` 时 `line` 为倍数:1=单倍、1.5=1.5倍、2=双倍;`before`/`after` 单位 pt)
|
||||
- `line?: number`, `before?: number`, `after?: number`
|
||||
- `lineRule?: "atLeast" | "auto" | "exact"`
|
||||
- `shd?: {val?, fill?, color?}` — 底纹
|
||||
- `quote?: boolean` — 引用标识(注:服务端也接受 `blockquote: true` 的别名)
|
||||
- `list?: object` — 列表标识(见下方 list 节点)
|
||||
- `refs?: string[]` — 脚注引用 ID(footnote 标识)
|
||||
- **children**: 一个 text 容器 + 可选的 inline 节点(link/img/tag/mention 等)
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["p", {"uuid": "p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "普通段落"]]]
|
||||
["p", {"uuid": "p2", "jc": "center"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "居中段落"]]]
|
||||
["p", {"uuid": "p3", "ind": {"firstLine": 32}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "首行缩进段落"]]]
|
||||
["p", {"uuid": "p4", "spacing": {"line": 1.5, "lineRule": "auto"}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "1.5倍行距"]]]
|
||||
```
|
||||
|
||||
### heading(标题)
|
||||
|
||||
- **tag**: `"h1"` | `"h2"` | `"h3"` | `"h4"` | `"h5"` | `"h6"`
|
||||
- **attrs**: 同 paragraph
|
||||
- **children**: 同 paragraph
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["h1", {"uuid": "h1a"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "一级标题"]]]
|
||||
["h3", {"uuid": "h3a", "jc": "center"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "居中三级标题"]]]
|
||||
```
|
||||
|
||||
### blockquote(引用)
|
||||
|
||||
- **tag**: `"p"` 或 `"h1"`~`"h6"`(不是独立 tag,是属性装饰)
|
||||
- **标识**: `attrs.quote: true`(也接受 `blockquote: true`)
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["p", {"uuid": "q1", "quote": true}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "这是一段引用"]]]
|
||||
["h2", {"uuid": "q2", "quote": true}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "引用标题"]]]
|
||||
```
|
||||
|
||||
### list(列表)
|
||||
|
||||
- **tag**: `"p"`(列表项在 JSONML 层面是扁平的段落,通过 `attrs.list` 标识)
|
||||
- **attrs.list**:
|
||||
- `listId: string` — **必传**。同一列表的项共享相同 ID。validator 报错:`必传字段缺失`
|
||||
- `level: number` — **必传**。缩进层级(0-based,≥0)。validator 报错:`必传字段缺失` / `必须 ≥0`
|
||||
- `isOrdered?: boolean` — 是否有序,默认 `false`
|
||||
- `isTaskList?: boolean` — 是否任务列表,默认 `false`
|
||||
- `isChecked?: boolean` — 任务是否完成(仅 isTaskList=true 时有意义)
|
||||
- `isCanceled?: boolean` — 任务是否取消
|
||||
- `start?: number` — 有序列表起始序号(≥1)。**仅在列表第一项设置**,后续项不设置此字段(系统自动递增)。validator 报错:`必须 ≥1`
|
||||
- `listStyleType?: string` — 样式类型(31 种预设)
|
||||
- `hideSymbol?: boolean` — 隐藏列表符号
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["p", {"uuid": "li1", "list": {"listId": "abc", "level": 0, "isOrdered": false}},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "无序列表项"]]]
|
||||
["p", {"uuid": "li2", "list": {"listId": "def", "level": 0, "isOrdered": true, "start": 1}},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "有序第一项"]]]
|
||||
["p", {"uuid": "li2b", "list": {"listId": "def", "level": 0, "isOrdered": true}},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "有序第二项(不设 start,自动编号 2)"]]]
|
||||
["p", {"uuid": "li3", "list": {"listId": "ghi", "level": 1, "isOrdered": false}},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "二级缩进"]]]
|
||||
["p", {"uuid": "li4", "list": {"listId": "jkl", "level": 0, "isTaskList": true, "isChecked": false}},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "待办事项"]]]
|
||||
```
|
||||
|
||||
- **注意**: 列表是扁平结构,不是嵌套的 ol/ul/li
|
||||
|
||||
### hr(分割线)
|
||||
|
||||
- **tag**: `"hr"`
|
||||
- **attrs**(全部 optional):
|
||||
- `type?: TLineStyle` — 16 种枚举(`"single"` / `"dotted"` / `"dashed"` / `"double"` / `"wave"` 等),默认 `"single"`
|
||||
- `sz?: number` — 粗细(px),默认 `1`
|
||||
- `color?: string` — 颜色
|
||||
- **children**: 构造时无需传子节点;服务端返回的真实文档中可能含内部配置数据子节点
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["hr", {"uuid": "hr1"}]
|
||||
["hr", {"uuid": "hr2", "type": "dashed", "color": "#ccc", "sz": 2}]
|
||||
```
|
||||
|
||||
### table(表格)
|
||||
|
||||
- **tag**: `"table"`
|
||||
- **attrs**:
|
||||
- `colsWidth: number[]` — 列宽(语义必传)。缺失时 validator 警告 `table 应提供 colsWidth`
|
||||
- 默认模式:值为各列绝对宽度,单位 **pt**(如 `[325, 325]` 总和≈页宽 650pt)
|
||||
- 比例模式(配合 `tblW: {"type": "pct"}`):值为百分比权重(如 `[33.3, 33.3, 33.4]` 总和=100)
|
||||
- `tblW?: {w?: number, type?: string}` — 表格宽度模式。`type: "pct"` 时 colsWidth 按比例解析
|
||||
- `sr?: boolean` — `true` 表示这是分栏布局(columns),不是普通表格
|
||||
- `jc?: string` — 对齐(源码注释"目前无消费")
|
||||
- **children**: `["tr", ...]` 行节点
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["table", {"uuid": "tb1", "colsWidth": [200, 200]},
|
||||
["tr", {"uuid": "tr1"},
|
||||
["tc", {"uuid": "tc1", "colSpan": 1, "rowSpan": 1},
|
||||
["p", {"uuid": "tcp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "单元格1"]]]],
|
||||
["tc", {"uuid": "tc2", "colSpan": 1, "rowSpan": 1},
|
||||
["p", {"uuid": "tcp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "单元格2"]]]]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
#### tr(表格行)
|
||||
|
||||
- **tag**: `"tr"`
|
||||
- **attrs**: `{h?: number, isTblHeader?: boolean}`
|
||||
|
||||
#### tc(表格单元格)
|
||||
|
||||
- **tag**: `"tc"`
|
||||
- **attrs**:
|
||||
- `colSpan?: number` — 横跨列数,默认 `1`。validator 报错:`必须 ≥1`
|
||||
- `rowSpan?: number` — 横跨行数,默认 `1`。validator 报错:`必须 ≥1`
|
||||
- `fill?: string` — 填充色
|
||||
- `vAlign?: "top" | "middle" | "bottom"` — 垂直对齐,默认 `"middle"`。validator 报错枚举不符(注意 `"center"` 不是合法值,应用 `"middle"`)
|
||||
- `bdr?: object` — 单元格边框
|
||||
- **children**: 任意块级节点数组;**至少包含一个 `p`**(即使空),否则单元格无法渲染光标
|
||||
|
||||
### code(代码块)
|
||||
|
||||
- **tag**: `"code"`
|
||||
- **attrs**(全部 optional):
|
||||
- `code?: string` — 代码内容,默认 `""`
|
||||
- `syntax?: string` — 语言标识,默认 `"plaintext"`
|
||||
- `theme?: string` — 主题枚举:`"default"` / `"light"` / `"dracula"` / `"github"` / `"cobalt"` / `"atomOneDark"` / `"oneLightPro"` / `"nightOwl"` / `"githubDark"` / `"realDracula"`。未知值 validator 警告 `未知主题`
|
||||
- `wrap?: boolean` — 自动换行,默认 `true`
|
||||
- `showLineNumber?: boolean` — 显示行号,默认 `true`
|
||||
- `title?: string` — 标题(≤1000 字符)
|
||||
- `fold?: boolean` — 是否折叠,默认 `false`
|
||||
- **children**: 构造时无需传子节点(代码存在 `attrs.code` 中)
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["code", {"uuid": "cd1", "syntax": "javascript", "code": "console.log('hello');"}]
|
||||
["code", {"uuid": "cd2", "syntax": "python", "code": "print('hello')", "theme": "dracula"}]
|
||||
```
|
||||
|
||||
### container(容器/高亮块)
|
||||
|
||||
- **tag**: `"container"`
|
||||
- **attrs**:
|
||||
- `subType: string` — **必传**。callout 为 `"colorBlocks"`。缺失时 validator 报错 `container 必须包含 subType`
|
||||
- `metadata?: object` — 自定义元数据(callout 用 `{bgcolor, border}`)
|
||||
- **children**: 任意块级节点
|
||||
- **示例(callout)**:
|
||||
|
||||
```json
|
||||
["container", {"uuid": "co1", "subType": "colorBlocks", "metadata": {"bgcolor": "#E8F2FE", "border": "#B3D4FC"}},
|
||||
["p", {"uuid": "co1p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "这是一段提示内容"]]]
|
||||
]
|
||||
```
|
||||
|
||||
### columns(分栏布局)
|
||||
|
||||
- **tag**: `"table"`(复用 table tag,通过 `sr: true` 区分)
|
||||
- **attrs**:
|
||||
- `sr: true` — 固定标识
|
||||
- `colsWidth?: number[]` — 各列宽度(pt),前端按比例换算为页面宽度
|
||||
- `spacing?: number` — 栏间距
|
||||
- **children**: 单个 `["tr", ...]`,内含多个 `["tc", ...]`
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["table", {"uuid": "col1", "sr": true, "colsWidth": [300, 300]},
|
||||
["tr", {"uuid": "colr"},
|
||||
["tc", {"uuid": "colc1"},
|
||||
["p", {"uuid": "colp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "左栏"]]]],
|
||||
["tc", {"uuid": "colc2"},
|
||||
["p", {"uuid": "colp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "右栏"]]]]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
### embed(嵌入文件)
|
||||
|
||||
- **tag**: `"embed"`(void 块)
|
||||
- **attrs**:
|
||||
- `src?: string` — 文件来源 URL(语义必传)
|
||||
- `name?: string` — 文件名
|
||||
- `type?: string` — 文件类型(`"pdf"` / `"xlsx"` / `"html"` / `"file"` 等)
|
||||
- `size?: number` — 文件大小
|
||||
- `viewType?: string` — 视图类型(如 `"preview"`)
|
||||
- `previewSize?: {height: number}` — 预览高度(px)
|
||||
- **children**: 无
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["embed", {"uuid": "em1", "name": "report.pdf", "type": "pdf", "src": "https://example.com/report.pdf", "size": 2048, "viewType": "preview", "previewSize": {"height": 600}}]
|
||||
```
|
||||
|
||||
### onlineVideo(在线视频)
|
||||
|
||||
- **tag**: `"onlineVideo"`(void 块)
|
||||
- **attrs**(全部 optional,但 src 语义必传):
|
||||
- `src?: string` — 视频地址
|
||||
- `type?: string` — 平台标识(`"bilibili"` / `"youku"` / `"mp4"` 等)
|
||||
- `poster?: string` — 封面图 URL
|
||||
- **children**: 无
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["onlineVideo", {"uuid": "ov1", "src": "https://example.com/video.mp4"}]
|
||||
["onlineVideo", {"uuid": "ov2", "src": "https://player.bilibili.com/player.html?aid=1", "type": "bilibili", "poster": "https://example.com/poster.jpg"}]
|
||||
```
|
||||
|
||||
### card(河图组件 / 富卡片)
|
||||
|
||||
- **tag**: `"card"`
|
||||
- **attrs**:
|
||||
- `cardType: string` — 卡片类型标识(如 `"groupChatCard"`、`"vote"`)
|
||||
- `metadata: {id: string, ...}` — 组件元数据,必须包含 id
|
||||
- `height?: number`
|
||||
- **children**: 服务端 serialize 通常返回带一个空的 span/leaf 占位子节点,写入时建议保留以避免反序列化差异
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["card", {"uuid": "cd1", "cardType": "vote", "metadata": {"id": "card_abc123"}},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, ""]]]
|
||||
```
|
||||
|
||||
- **注意**: card 节点的重数据存储在独立 parts 层,body 中只存轻量引用
|
||||
|
||||
### toc(目录)
|
||||
|
||||
- **tag**: `"toc"`(void 块)
|
||||
- **attrs**(全部 **required**,缺失逐个 validator 报错):
|
||||
- `title: string` — 目录标题
|
||||
- `mode: "outline" | "column"` — 展示模式(其他值 validator 报错 `无效值`)
|
||||
- `styles: object` — 样式配置
|
||||
- `styles.global: {maxLevel: number, bgColor: string, css: object}`
|
||||
- `styles.title: {font: string, color: string, numbering: boolean, css: object}`
|
||||
- `styles.item: {symbol: "disc" | "none", css: object}`
|
||||
- `content: TocItem[]` — 目录条目数组;**写入时填 `[]` 即可**,服务端基于文档 heading 自动重建
|
||||
- 每项: `{uuid, anchorId: string|null, level, children: TocItem[]}`
|
||||
- **children**: 无
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["toc", {"uuid": "toc1", "title": "目录", "mode": "outline",
|
||||
"styles": {
|
||||
"global": {"maxLevel": 5, "bgColor": "#F0EBF7", "css": {}},
|
||||
"title": {"font": "", "color": "#000", "numbering": false, "css": {}},
|
||||
"item": {"symbol": "disc", "css": {}}
|
||||
},
|
||||
"content": []
|
||||
}]
|
||||
```
|
||||
|
||||
### refblock(引用块)
|
||||
|
||||
- **tag**: `"refblock"`
|
||||
- **attrs**:
|
||||
- `docKey?: string` — 所属/被引文档标识(语义必传)
|
||||
- `refblockUUID?: string` — 引用块唯一标识(语义必传)
|
||||
- **children**: 块级节点(降级显示快照;真实内容由服务端按 docKey/refblockUUID 拉取覆写)
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["refblock", {"uuid": "rb1", "docKey": "doc123", "refblockUUID": "block456"},
|
||||
["p", {"uuid": "rb1p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "(引用预览内容,服务端会回填)"]]]
|
||||
]
|
||||
```
|
||||
|
||||
- **注意**: 主块 (host):`docKey === refblockUUID`;副块 (copy):`docKey !== refblockUUID`
|
||||
|
||||
---
|
||||
|
||||
## Inline 节点
|
||||
|
||||
所有 inline 节点 tag 白名单(validator `validInlineTags`):
|
||||
`text`(legacy) / `a` / `img` / `span` / `tag` / `inlineCode` / `br` / `cangjie-textinline` / `cangjie-voidinline`
|
||||
|
||||
block tag 也可出现在 inline 上下文(如 `img` 既是 block 又是 inline)。未在白名单的 tag 会触发警告并给出 Levenshtein 建议。
|
||||
|
||||
### a(链接)
|
||||
|
||||
- **tag**: `"a"`
|
||||
- **attrs**(全部 optional,但 href 语义必传):
|
||||
- `href?: string` — 链接地址(缺失时 validator 警告 `link 缺少 href`)
|
||||
- `cardInfo?: object` — 链接卡片信息
|
||||
- `cardInfo.displayType?: "link" | "card"` — 展示模式
|
||||
- `cardInfo.title?: string`, `cardInfo.desc?: string`, `cardInfo.imgURL?: string`
|
||||
- `metadata?: object` — 扩展业务信息
|
||||
- **children**: 链接的展示文本(直接字符串,与 block 上下文不同)
|
||||
- **示例**(作为 paragraph 的 inline 子节点):
|
||||
|
||||
```json
|
||||
["p", {"uuid": "p1"},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "请访问"]],
|
||||
["a", {"href": "https://example.com"}, "链接文字"]
|
||||
]
|
||||
```
|
||||
|
||||
### img(图片)
|
||||
|
||||
- **tag**: `"img"`(既可作 block 也可作 inline 子节点)
|
||||
- **attrs**(全部 optional,但 src 语义必传):
|
||||
- `src?: string` — 图片地址
|
||||
- `width?: number` — 宽度(px)
|
||||
- `height?: number` — 高度(px)
|
||||
- `rectClip?: {left?, right?, top?, bottom?}` — 裁剪比例(0-1)
|
||||
- `rotation?: number` — 旋转角度
|
||||
- `radius?: number` — 圆角(px)
|
||||
- `shadow?: string` — CSS shadow
|
||||
- `outline?: {width?, type?, color?}` — 边框
|
||||
- **children**: 构造时无需传子节点;真实文档中可能含内部配置数据
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["img", {"uuid": "img1", "src": "https://example.com/photo.png", "width": 400, "height": 300}]
|
||||
```
|
||||
|
||||
- **注意**:
|
||||
- 不存在 `layout` 属性,布局由 UI 层控制
|
||||
- `img` 是 inline 元素,必须包裹在 `p` 段落中
|
||||
|
||||
### mention(@提及)
|
||||
|
||||
- **tag**: `"span"`
|
||||
- **attrs**:
|
||||
- `data-type: "mention"` — **必传**,固定标识
|
||||
- `id?: string` — 被@用户 ID(语义必传)
|
||||
- `name?: string` — 被@用户名(语义必传)
|
||||
- `login?: string` — 登录名
|
||||
- `metadata?: object` — 扩展元数据
|
||||
- **children**: 显示文本
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["span", {"data-type": "mention", "id": "user123", "name": "张三"}, "@张三"]
|
||||
```
|
||||
|
||||
### tag(通用标签节点)
|
||||
|
||||
- **tag**: `"tag"`
|
||||
- **attrs**:
|
||||
- `tagType: string` — **必传**。子类型标识(如 `"formula"` / `"imTag"`)
|
||||
- 其他属性依 tagType 而异
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["tag", {"tagType": "imTag", "text": "#标签名"}]
|
||||
```
|
||||
|
||||
### formula(公式)— `tag` 节点的特化
|
||||
|
||||
- **tag**: `"tag"`
|
||||
- **attrs**:
|
||||
- `tagType: "formula"` — **必传**,固定值
|
||||
- `metadata: {formula: string}` — **必传**。LaTeX 代码(空串表示空公式)。缺失或类型错时 validator 报错
|
||||
- **children**: 无
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["tag", {"tagType": "formula", "metadata": {"formula": "E=mc^2"}}]
|
||||
```
|
||||
|
||||
### sticker(表情贴纸)
|
||||
|
||||
- **tag**: `"span"`
|
||||
- **attrs**:
|
||||
- `data-type: "emoji"` — **必传**(注意:`"emoji"` 不是 `"sticker"`)
|
||||
- `code?: string` — 表情纯文本标识(如 `"[微笑]"`)
|
||||
- `newCode?: IEmoji` — 完整表情对象,5 种子类型:
|
||||
- `{type: "dingding", id, name, url}`
|
||||
- `{type: "unicode", value}`
|
||||
- `{type: "custom", url}`
|
||||
- `{type: "icon", id, color?}`
|
||||
- `{type: "svg", id, url, color?}`
|
||||
- **children**: 无或空文本
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["span", {"data-type": "emoji", "code": "[微笑]", "newCode": {"type": "unicode", "value": "😊"}}]
|
||||
```
|
||||
|
||||
### inlineCode(行内代码)
|
||||
|
||||
- **tag**: `"inlineCode"`
|
||||
- **attrs**: `{bgColor?: string}` 或 `{}`
|
||||
- **children**: 文本(inline 子节点)
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["inlineCode", {}, "const x = 1"]
|
||||
```
|
||||
|
||||
### br(换行符)
|
||||
|
||||
- **tag**: `"br"`(void inline)
|
||||
- **attrs**: `{}`
|
||||
- **children**: 空文本占位
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["br", {}, ""]
|
||||
```
|
||||
|
||||
### refer(行内引用)
|
||||
|
||||
- **tag**: `"span"`
|
||||
- **attrs**:
|
||||
- `data-type: "refer"` — 固定标识
|
||||
- 其他业务属性(自由 key-value)
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["span", {"data-type": "refer", "docId": "xxx", "blockId": "yyy"}, "引用内容"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cangjie 系列(扩展块)
|
||||
|
||||
### attachment(附件)
|
||||
|
||||
- **tag**: `"cangjie-voidblock"` 或 `"cangjie-voidinline"`
|
||||
- **attrs**:
|
||||
- `subType: "attachment"` — 固定标识
|
||||
- `data.viewType: "preview" | "abstractCard"` — 视图类型
|
||||
- `data.fileData: {name?, src?, size?, fileType?, category: "media" | "file"}` — 文件数据
|
||||
- `data.previewSize?: {width?, height?}` — 预览尺寸
|
||||
- **children**: 无
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["cangjie-voidblock", {"subType": "attachment", "data": {"viewType": "abstractCard", "fileData": {"name": "report.pdf", "src": "https://...", "size": 2048, "category": "file"}}}]
|
||||
```
|
||||
|
||||
- **注意**: 反序列化时匹配 `"embed"` tag 并转换为 attachment
|
||||
|
||||
### footnote(脚注)
|
||||
|
||||
- **tag**: 宿主节点 tag(`"p"` / `"h1"`~`"h6"`),不是独立节点
|
||||
- **标识**: `attrs.refs: string[]`(脚注引用 ID 数组)
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["p", {"uuid": "fn1", "refs": ["footnote-id-1"]},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "正文文本"]]]
|
||||
```
|
||||
|
||||
- **注意**: footnote 将 `refs` 属性注入到宿主 block 节点中,类似 blockquote 的属性装饰模式
|
||||
|
||||
### calendar(日程)
|
||||
|
||||
- **tag**: `"cangjie-voidinline"` 或 `"cangjie-voidblock"`
|
||||
- **attrs**:
|
||||
- `subType: "calendar"` — 固定标识
|
||||
- `data.viewType: "inlineCalendar" | "blockCalendar"` — 形态
|
||||
- `data.calendarId: string` — 日程 ID
|
||||
- `data.subject: string` — 日程名称
|
||||
- `data.detailUrl: string` — 日程详情链接
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["cangjie-voidinline", {"subType": "calendar", "data": {"viewType": "inlineCalendar", "calendarId": "cal-001", "subject": "周会", "detailUrl": "https://..."}}]
|
||||
```
|
||||
|
||||
### label(标签)
|
||||
|
||||
- **tag**: `"cangjie-textinline"`
|
||||
- **attrs**:
|
||||
- `subType: "label"` — 固定标识
|
||||
- `data.bgColor?: string` — 标签背景色
|
||||
- `data.color?: string` — 标签文字颜色
|
||||
- `data.labelType?: "normal" | "note" | "spoiler"` — 标签模式
|
||||
- **children**: 文本内容
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["cangjie-textinline", {"subType": "label", "data": {"bgColor": "#FFE8CC", "color": "#D46B08", "labelType": "normal"}}, "重要"]
|
||||
```
|
||||
|
||||
### templateButton(模板按钮)
|
||||
|
||||
- **tag**: `"cangjie-container"`
|
||||
- **attrs**:
|
||||
- `subType: "templateButton"` — 固定标识
|
||||
- `metadata.direction: "top" | "bottom"` — 按钮方向
|
||||
- `metadata.isOnce: boolean` — 是否一次性
|
||||
- `metadata.title: string` — 按钮标题
|
||||
- **children**: 块级节点数组
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["cangjie-container", {"subType": "templateButton", "metadata": {"direction": "bottom", "isOnce": false, "title": "添加待办"}},
|
||||
["p", {"uuid": "tb1p1", "list": {"level": 0, "isChecked": false, "isOrdered": false, "isTaskList": true, "listId": "abc"}},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, ""]]]
|
||||
]
|
||||
```
|
||||
|
||||
### textSlot(文本插槽)
|
||||
|
||||
- **tag**: `"cangjie-textinline"`
|
||||
- **attrs**:
|
||||
- `subType: "textSlot"` — 固定标识
|
||||
- `data.slotInfo.style.color: string` — 文字颜色(支持渐变色)
|
||||
- **children**: 文本内容
|
||||
- **示例**:
|
||||
|
||||
```json
|
||||
["cangjie-textinline", {"subType": "textSlot", "data": {"slotInfo": {"style": {"color": "#1890ff"}}}}, "插槽文本"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整文档示例
|
||||
|
||||
```json
|
||||
["root", {"sectPr": {"pgSz": {"w": 11906, "h": 16838}}},
|
||||
["h1", {"uuid": "h1"},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "文档标题"]]],
|
||||
|
||||
["p", {"uuid": "p1"},
|
||||
["span", {"data-type": "text"},
|
||||
["span", {"data-type": "leaf"}, "这是一段普通文本,包含"],
|
||||
["span", {"data-type": "leaf", "bold": true}, "加粗"],
|
||||
["span", {"data-type": "leaf"}, "和"]],
|
||||
["a", {"href": "https://example.com"}, "链接"],
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "。"]]],
|
||||
|
||||
["h2", {"uuid": "h2"},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "列表示例"]]],
|
||||
["p", {"uuid": "li1", "list": {"listId": "l1", "level": 0, "isOrdered": true, "start": 1}},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "第一项"]]],
|
||||
["p", {"uuid": "li2", "list": {"listId": "l1", "level": 0, "isOrdered": true}},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "第二项"]]],
|
||||
["p", {"uuid": "li3", "list": {"listId": "l1", "level": 1, "isOrdered": false}},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "子项"]]],
|
||||
|
||||
["hr", {"uuid": "hr1"}],
|
||||
|
||||
["code", {"uuid": "cd1", "syntax": "javascript", "code": "function hello() {\n return 'world';\n}"}],
|
||||
|
||||
["container", {"uuid": "co1", "subType": "colorBlocks", "metadata": {"bgcolor": "#E8F2FE"}},
|
||||
["p", {"uuid": "co1p1"},
|
||||
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "这是一个提示块"]]]],
|
||||
|
||||
["table", {"uuid": "tb1", "colsWidth": [200, 200]},
|
||||
["tr", {"uuid": "tr1"},
|
||||
["tc", {"uuid": "tc1", "colSpan": 1, "rowSpan": 1},
|
||||
["p", {"uuid": "tcp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "A"]]]],
|
||||
["tc", {"uuid": "tc2", "colSpan": 1, "rowSpan": 1},
|
||||
["p", {"uuid": "tcp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "B"]]]]
|
||||
]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
## 设计要点
|
||||
|
||||
1. **Canonical 文本是 span/leaf**:每段文字 = `["span", {"data-type":"text"}, ["span", {"data-type":"leaf", ...marks}, "..."]]`。legacy `["text", {marks}, "..."]` 仍被接受但不建议新写。
|
||||
2. **裸字符串作 block 子节点违法**:validator 报错,请手动包成 canonical 形式。
|
||||
3. **每个 block 必带 `uuid`**:手写 JSONML 时建议每个 block 自带 `uuid`(base32 alphanumeric,dws CLI 用 `dws` 前缀)。
|
||||
4. **扁平列表**: 列表不嵌套,通过 `listId` + `level` 表达层级。
|
||||
5. **属性装饰**: blockquote / list / footnote 不是独立 tag,是 paragraph 的属性。
|
||||
6. **Void 节点**: hr / code / img / card / toc / embed / onlineVideo 在构造时不需要传子节点;服务端返回的真实文档中这些节点**可能包含内部配置数据子节点**,解析时应兼容。
|
||||
7. **columns = table + sr:true**: 分栏复用表格结构。
|
||||
8. **card 轻引用**: body 中只存 cardType + metadata.id,重数据在 parts 层。
|
||||
9. **root 节点**: 服务端返回的完整 body 以 `["root", {sectPr...}, ...blocks]` 包裹。`doc create/update` 写入时必须以 root 为根节点,缺少会报错。`doc block insert/update` 不要求 root。
|
||||
Reference in New Issue
Block a user