mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-07-03 14:09:06 +02:00
📝 File over app: .sy is open JSON that never locks in user data https://github.com/siyuan-note/siyuan/issues/17961
Signed-off-by: Daniel <845765@qq.com>
This commit is contained in:
+592
@@ -0,0 +1,592 @@
|
||||
# SiYuan `.sy` File JSON Structure — AI Read/Write Guide
|
||||
|
||||
> Spec baseline: `2` (current across all files).
|
||||
> Verified against samples: `20200825162036-4dx365o.sy` (formatting elements), `20200905090211-2vixtlf.sy` (block types).
|
||||
> All conclusions are based on real samples and the Lute / SiYuan kernel source. Fields marked `【inferred】` were not directly observed in the samples — re-verify against a real sample before generating them.
|
||||
|
||||
## 0. In one sentence
|
||||
|
||||
A `.sy` file is a Lute AST tree serialized to JSON. The root node is always `NodeDocument`; the body is the `Children` array, recursively nested. There is no external schema — all state lives in the tree.
|
||||
|
||||
---
|
||||
|
||||
## 0.5 When to read/write `.sy` directly (priority order)
|
||||
|
||||
SiYuan offers three official paths to mutate data: **HTTP API, MCP, and CLI**. **Prefer them by default.** The kernel handles AST serialization, block-ID allocation, and synchronization of two indexes: the block-tree index (`blocktree.db`, the block-ID → file-path map that block refs and breadcrumbs depend on) and the full-text search index (`siyuan.db` + FTS5). Writing the files directly bypasses all of this and easily leaves the indexes out of sync.
|
||||
|
||||
**Only read/write `.sy` as JSON when the official paths are inconvenient.** Applicable scenarios:
|
||||
- Bulk offline migration (cold-init a workspace, import external data)
|
||||
- Read-only statistics, analysis, custom export / format conversion
|
||||
- Repairing low-level structural issues (legacy files, illegal nodes)
|
||||
- Programmatic scaffolding / template generation
|
||||
|
||||
Division of labor among the four paths:
|
||||
|
||||
| Path | Role | Mutation capability |
|
||||
|---|---|---|
|
||||
| **HTTP API** | Online, at runtime | Richest — full CRUD on docs/blocks (`filetree/*`, `block/*`, `transactions`) |
|
||||
| **MCP** | LLM tool set | Subset for AI agents operating on docs online |
|
||||
| **CLI** | Batch / ops | Import, export, sync, SQL, and other command-line tasks |
|
||||
| **Read/write `.sy` directly** | The scope of this guide | Offline, bulk, low-level structural work |
|
||||
|
||||
> ⚠️ After writing files directly you usually need a "rebuild index" pass before search/block-refs become effective. If SiYuan is running, prefer the HTTP API and let the kernel handle serialization and index sync.
|
||||
|
||||
---
|
||||
|
||||
## 1. Top-level structure
|
||||
|
||||
```json
|
||||
{
|
||||
"ID": "20200825162036-4dx365o",
|
||||
"Spec": "2",
|
||||
"Type": "NodeDocument",
|
||||
"Properties": {
|
||||
"icon": "1f4f0",
|
||||
"id": "20200825162036-4dx365o",
|
||||
"title": "排版元素",
|
||||
"type": "doc",
|
||||
"updated": "20260616224229"
|
||||
},
|
||||
"Children": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
| Top-level key | Required | Meaning |
|
||||
|---|---|---|
|
||||
| `ID` | ✅ | Document block ID. **Equals the filename without `.sy`** (not randomly generated) |
|
||||
| `Spec` | ✅ | Always `"2"` |
|
||||
| `Type` | ✅ | Always `"NodeDocument"` |
|
||||
| `Properties` | ✅ | Document-level IAL — see §8 |
|
||||
| `Children` | ✅ | Array of body child blocks; must contain at least one block |
|
||||
|
||||
> ⚠️ The file path strictly corresponds to the root ID: `data/<box>/<...>/<rootID>.sy`. Changing the root ID means renaming the file — don't change it casually.
|
||||
|
||||
---
|
||||
|
||||
## 2. Common field semantics (apply to every node)
|
||||
|
||||
| Field | Type | Presence | Meaning |
|
||||
|---|---|---|---|
|
||||
| `Type` | string | **required on every node** | Type discriminator, e.g. `"NodeParagraph"` |
|
||||
| `ID` | string | block nodes only | 22-char block ID; inline/marker nodes **do not carry it** |
|
||||
| `Data` | string | some | Text / HTML / markdown raw; **may be omitted** (don't assume it exists) |
|
||||
| `Properties` | object | most blocks | IAL, `map[string]string` |
|
||||
| `Children` | array | container / nestable nodes | Child node array |
|
||||
| Type-specific fields | - | per type | e.g. `HeadingLevel`, `ListData`, `TextMarkType`, `AttributeViewID` |
|
||||
|
||||
**Core discriminator rule:** whether a node has an `ID` determines whether it is a block. Has `ID` ⇒ block (its `Properties.id` must equal its `ID`); no `ID` ⇒ inline/marker node.
|
||||
|
||||
---
|
||||
|
||||
## 3. ID and timestamp rules
|
||||
|
||||
- **ID format:** `YYYYMMDDHHMMSS-xxxxxxx` = 14-digit timestamp + `-` + 7 random `[a-z0-9]` chars. Example: `20210104091228-ttcj9nm`.
|
||||
- The **root ID** comes from the filename and is **not** regenerated.
|
||||
- **Child block IDs** are generated with the above scheme.
|
||||
- `Properties.updated` is the same 14-digit timestamp; semantics: "last updated time".
|
||||
- When you change any block's `ID`, you must **sync** `Properties.id`. `Properties.updated` should be refreshed to the current time as well.
|
||||
|
||||
---
|
||||
|
||||
## 4. Node-type catalog
|
||||
|
||||
### Block nodes (have an ID)
|
||||
|
||||
**Leaf blocks:** `NodeParagraph`, `NodeHeading`, `NodeThematicBreak`, `NodeHTMLBlock`, `NodeCodeBlock`, `NodeMathBlock`, `NodeTable`, `NodeBlockQueryEmbed`, `NodeAttributeView`, `NodeIFrame`, `NodeVideo`, `NodeAudio`, `NodeWidget`, `NodeCustomBlock`, `NodeGitConflict`
|
||||
|
||||
**Container blocks:** `NodeList`, `NodeListItem`, `NodeBlockquote`, `NodeCallout`, `NodeSuperBlock`
|
||||
|
||||
**Block-level content children** (live inside a block, also have an ID): `NodeCodeBlockCode`, `NodeMathBlockContent`
|
||||
|
||||
### Inline / marker nodes (no ID)
|
||||
|
||||
`NodeText`, `NodeTextMark`, `NodeImage`, `NodeKramdownSpanIAL`, `NodeHeadingC8hMarker`, `NodeBlockquoteMarker`, `NodeTaskListItemMarker`, `NodeBang`, `NodeOpenBracket`, `NodeCloseBracket`, `NodeOpenParen`, `NodeCloseParen`, `NodeLinkText`, `NodeLinkDest`, `NodeCodeBlockFenceOpenMarker`, `NodeCodeBlockFenceInfoMarker`, `NodeCodeBlockFenceCloseMarker`, `NodeMathBlockOpenMarker`, `NodeMathBlockCloseMarker`, `NodeSuperBlockOpenMarker`, `NodeSuperBlockLayoutMarker`, `NodeSuperBlockCloseMarker`, `NodeOpenBrace`, `NodeCloseBrace`, `NodeBlockQueryEmbedScript`, `NodeTableHead`, `NodeTableRow`, `NodeTableCell`
|
||||
|
||||
> Types disabled by SiYuan (footnotes / ToC / YAML / LinkRef / HeadingID, etc.) are listed in §11 and never appear in a `.sy` — they are intentionally excluded from this catalog.
|
||||
|
||||
---
|
||||
|
||||
## 5. Block types in detail (with copyable samples)
|
||||
|
||||
### 5.1 Paragraph
|
||||
|
||||
```json
|
||||
{ "Type": "NodeParagraph", "ID": "...", "Properties": { "id": "...", "updated": "..." },
|
||||
"Children": [ { "Type": "NodeText", "Data": "This is a sample paragraph." } ] }
|
||||
```
|
||||
|
||||
### 5.2 Heading
|
||||
|
||||
```json
|
||||
{ "Type": "NodeHeading", "ID": "...", "HeadingLevel": 2,
|
||||
"Properties": { "id": "...", "updated": "..." },
|
||||
"Children": [ { "Type": "NodeText", "Data": "Heading" } ] }
|
||||
```
|
||||
|
||||
- `HeadingLevel` ranges `1`–`6`.
|
||||
- `NodeHeadingC8hMarker` (`Data` such as `"## "`) is **optional** — present or absent, both are legal. Recommend **omitting** it for brevity when generating.
|
||||
- SiYuan recommends using **level-2 headings at the top of the body**, not level 1.
|
||||
|
||||
### 5.3 Lists (key: distinguish type via `ListData.Typ`)
|
||||
|
||||
> **★ Hard structural constraint of lists:** direct children of `NodeList` can **only** be `NodeListItem` (enforced by Lute's `CanContain`, `ast/node.go:993` `return NodeListItem == nodeType`). Paragraphs, code blocks, sub-lists, or any other block **cannot** be attached directly under `NodeList` — they must be wrapped in a `NodeListItem` first.
|
||||
|
||||
```
|
||||
✅ Correct ❌ Wrong
|
||||
NodeList NodeList
|
||||
└─ NodeListItem ├─ NodeParagraph ← illegal
|
||||
└─ NodeParagraph └─ NodeCodeBlock ← illegal
|
||||
```
|
||||
|
||||
**Nested lists** are written by wrapping another `NodeList` (`NodeListItem` falls into the default `CanContain` branch and cannot directly contain another `NodeListItem`):
|
||||
|
||||
```
|
||||
✅ Correct ❌ Wrong
|
||||
NodeList NodeList
|
||||
└─ NodeListItem └─ NodeListItem
|
||||
├─ NodeParagraph ├─ NodeParagraph
|
||||
└─ NodeList ← sub-list └─ NodeListItem ← illegal
|
||||
└─ NodeListItem
|
||||
└─ NodeParagraph
|
||||
```
|
||||
|
||||
**Unordered list** (`Typ` omitted):
|
||||
|
||||
```json
|
||||
{ "Type": "NodeList", "ID": "...", "ListData": {},
|
||||
"Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeListItem", "ID": "...",
|
||||
"ListData": { "BulletChar": 42, "Marker": "Kg==" },
|
||||
"Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeParagraph", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [ { "Type": "NodeText", "Data": "Item one" } ] }
|
||||
] }
|
||||
] }
|
||||
```
|
||||
|
||||
**Ordered list** (`Typ: 1`):
|
||||
|
||||
```json
|
||||
{ "Type": "NodeListItem", "ID": "...", "Data": "1",
|
||||
"ListData": { "Typ": 1, "Tight": true, "Start": 1, "Delimiter": 46, "Padding": 3, "Marker": "MQ==", "Num": 1 },
|
||||
"Properties": { "id": "..." }, "Children": [ ... ] }
|
||||
```
|
||||
|
||||
**Task list** (`Typ: 3`); the first child is a `NodeTaskListItemMarker`:
|
||||
|
||||
```json
|
||||
{ "Type": "NodeListItem", "ID": "...",
|
||||
"ListData": { "Typ": 3, "Tight": true, "BulletChar": 45, "Padding": 2, "Checked": true, "Marker": "LQ==", "Num": -1 },
|
||||
"Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeTaskListItemMarker", "Data": "[X]", "TaskListItemChecked": true },
|
||||
{ "Type": "NodeParagraph", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [ { "Type": "NodeText", "Data": "Task one" } ] }
|
||||
] }
|
||||
```
|
||||
|
||||
### 5.4 `ListData` fields in full (★ easiest to get wrong)
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `Typ` | int | **List type discriminator:** omitted = unordered, `1` = ordered, `3` = task |
|
||||
| `Tight` | bool | Tight (no blank lines); optional |
|
||||
| `BulletChar` | int | Bullet **ASCII codepoint** for unordered/task lists (`42` = `*`, `45` = `-`) |
|
||||
| `Delimiter` | int | Ordered-list delimiter ASCII codepoint (`46` = `.`) |
|
||||
| `Start` | int | Ordered-list start number |
|
||||
| `Num` | int | This item's number; **always `-1` for unordered/task lists** |
|
||||
| `Padding` | int | Indent padding; optional |
|
||||
| `Checked` | bool | Whether the task item is checked (aggregated at the list level) |
|
||||
| `Marker` | string(base64) | The marker text, **base64-encoded**; may include a delimiter (`"MS4="` = `1.`) or not (`"MQ=="` = `1`) |
|
||||
|
||||
> Key distinction: **`BulletChar`/`Delimiter` are int codepoints**; **`Marker` is a base64 string**. Easy to confuse.
|
||||
|
||||
### 5.5 Task marker
|
||||
|
||||
```json
|
||||
// Checked
|
||||
{ "Type": "NodeTaskListItemMarker", "Data": "[X]", "TaskListItemChecked": true }
|
||||
// Unchecked
|
||||
{ "Type": "NodeTaskListItemMarker", "Data": "[ ]" }
|
||||
```
|
||||
|
||||
> The checked-state `Data` is uppercase `[X]`, **not** `[x]`. Unchecked may omit `TaskListItemChecked` (default false).
|
||||
|
||||
### 5.6 Blockquote
|
||||
|
||||
```json
|
||||
{ "Type": "NodeBlockquote", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeBlockquoteMarker", "Data": "> " },
|
||||
{ "Type": "NodeParagraph", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [ { "Type": "NodeText", "Data": "Quoted content" } ] }
|
||||
] }
|
||||
```
|
||||
|
||||
> `NodeBlockquoteMarker.Data` may be `">"` or `"> "` — both are legal.
|
||||
|
||||
### 5.7 Callout (GFM Alert)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeCallout", "ID": "...",
|
||||
"CalloutType": "NOTE", "CalloutTitle": "Note", "CalloutIcon": "✏️",
|
||||
"Properties": { "id": "...", "updated": "..." },
|
||||
"Children": [ { "Type": "NodeParagraph", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [ { "Type": "NodeText", "Data": "Callout content" } ] } ] }
|
||||
```
|
||||
|
||||
| `CalloutType` | `CalloutTitle` | `CalloutIcon` |
|
||||
|---|---|---|
|
||||
| `NOTE` | `Note` | `✏️` |
|
||||
| `TIP` | `Tip` | `💡` |
|
||||
| `IMPORTANT` | `Important` | `❗` |
|
||||
| `WARNING` | `Warning` | `⚠️` |
|
||||
| `CAUTION` | `Caution` | `🚨` |
|
||||
|
||||
> `CalloutIcon` is a **literal emoji character**, not base64, not a codepoint.
|
||||
|
||||
### 5.8 Super block (nestable; three-part structure)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeSuperBlock", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeSuperBlockOpenMarker" },
|
||||
{ "Type": "NodeSuperBlockLayoutMarker", "Data": "col" },
|
||||
{ "Type": "NodeSuperBlock", "ID": "...", "Properties": { "id": "..." }, "Children": [ ... nested super block, Data "row" ... ] },
|
||||
{ "Type": "NodeSuperBlockCloseMarker" }
|
||||
] }
|
||||
```
|
||||
|
||||
> `NodeSuperBlockLayoutMarker.Data` can only be `"row"` (horizontal) or `"col"` (vertical). Super blocks can nest, and it is the only container that can hold any block (including itself).
|
||||
|
||||
### 5.9 Embed block (five-part structure `{{ ... }}`)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeBlockQueryEmbed", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeOpenBrace" },
|
||||
{ "Type": "NodeOpenBrace" },
|
||||
{ "Type": "NodeBlockQueryEmbedScript", "Data": "select * from blocks where id='20210428212840-8rqwn5o'" },
|
||||
{ "Type": "NodeCloseBrace" },
|
||||
{ "Type": "NodeCloseBrace" }
|
||||
] }
|
||||
```
|
||||
|
||||
### 5.10 Code block (four-part structure; fenced only)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeCodeBlock", "ID": "...", "IsFencedCodeBlock": true,
|
||||
"CodeBlockFenceChar": 96, "CodeBlockFenceLen": 3,
|
||||
"CodeBlockOpenFence": "YGBg", "CodeBlockInfo": "Z28=", "CodeBlockCloseFence": "YGBg",
|
||||
"Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeCodeBlockFenceOpenMarker", "Data": "```", "CodeBlockFenceLen": 3 },
|
||||
{ "Type": "NodeCodeBlockFenceInfoMarker", "CodeBlockInfo": "Z28=" },
|
||||
{ "Type": "NodeCodeBlockCode", "ID": "...", "Data": "package main\n...\n", "Properties": { "id": "..." } },
|
||||
{ "Type": "NodeCodeBlockFenceCloseMarker", "Data": "```", "CodeBlockFenceLen": 3 }
|
||||
] }
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `NodeCodeBlockCode` is **block-level** (has `ID`, `Properties`); content goes in `Data` (raw text, `\n` escaped).
|
||||
- The surrounding markers are inline (no ID).
|
||||
- `CodeBlockInfo` is the **base64-encoded language** (`"Z28="` = `go`). The three fence fields (`CodeBlockOpenFence`/`CloseFence` are both `"YGBg"` = ` ``` `) and `CodeBlockFenceChar`/`FenceLen` are **optional** (in the shorthand form they can be omitted, using only `IsFencedCodeBlock: true`).
|
||||
- SiYuan **does not support indented code blocks** (`SetIndentCodeBlock(false)`); all code blocks are fenced.
|
||||
|
||||
### 5.11 Math block (three-part structure)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeMathBlock", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeMathBlockOpenMarker" },
|
||||
{ "Type": "NodeMathBlockContent", "ID": "...", "Data": "a^2 + b^2 = c^2", "Properties": { "id": "..." } },
|
||||
{ "Type": "NodeMathBlockCloseMarker" }
|
||||
] }
|
||||
```
|
||||
|
||||
### 5.12 HTML / IFrame / Video / Audio blocks (leaf; content in the top-level `Data`)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeHTMLBlock", "ID": "...", "Data": "<div>\n<ruby>你<rt>nǐ</rt>...</div>", "Properties": { "id": "..." } }
|
||||
{ "Type": "NodeIFrame", "ID": "...", "Data": "<iframe src=\"...\"></iframe>", "Properties": { "id": "..." } }
|
||||
{ "Type": "NodeVideo", "ID": "...", "Data": "<video controls src=\"assets/x.mp4\"></video>", "Properties": { "id": "..." } }
|
||||
{ "Type": "NodeAudio", "ID": "...", "Data": "<audio controls src=\"assets/x.wav\"></audio>", "Properties": { "id": "..." } }
|
||||
```
|
||||
|
||||
> These four **have no `Children`**; the HTML content (JSON-escaped) goes directly in the top-level `Data`.
|
||||
|
||||
### 5.13 Table
|
||||
|
||||
```json
|
||||
{ "Type": "NodeTable", "ID": "...", "TableAligns": [0, 0, 0],
|
||||
"Properties": { "id": "...", "colgroup": "||" },
|
||||
"Children": [
|
||||
{ "Type": "NodeTableHead", "Data": "thead", "Children": [
|
||||
{ "Type": "NodeTableRow", "Data": "tr", "Children": [
|
||||
{ "Type": "NodeTableCell", "Data": "th", "Children": [ { "Type": "NodeText", "Data": "Header" } ] }
|
||||
] }
|
||||
] },
|
||||
{ "Type": "NodeTableRow", "Data": "tr", "Children": [
|
||||
{ "Type": "NodeTableCell", "Data": "td", "Children": [ { "Type": "NodeText", "Data": "Cell" } ] }
|
||||
] }
|
||||
] }
|
||||
```
|
||||
|
||||
- Nesting is fixed: `NodeTable > NodeTableHead/NodeTableRow > NodeTableCell > inline`.
|
||||
- `TableAligns`: int array of per-column alignment, `0` = default/left.
|
||||
- `Data` (`thead`/`tr`/`th`/`td`) may be **omitted** in compact files.
|
||||
- Column widths are stored in `Properties.colgroup` (`|`-separated).
|
||||
|
||||
### 5.14 AttributeView block (database; leaf)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeAttributeView", "ID": "...",
|
||||
"Properties": { "custom-sy-av-view": "20251230141609-lcme2fh", "id": "...", "updated": "..." },
|
||||
"AttributeViewID": "20251230141609-2kvghrg",
|
||||
"AttributeViewType": "table" }
|
||||
```
|
||||
|
||||
- **Has no `Children`.**
|
||||
- `AttributeViewID` points to the AV table data (stored in a separate `.json` — **don't** fabricate this ID).
|
||||
- `AttributeViewType`: `table` / `kanban` / `gallery`, etc.
|
||||
- `custom-sy-av-view` records the current view ID.
|
||||
|
||||
> AI is advised **not to create new AttributeView blocks**, since the table data is not in the `.sy` — it requires accompanying files.
|
||||
|
||||
### 5.15 Thematic break
|
||||
|
||||
```json
|
||||
{ "Type": "NodeThematicBreak", "ID": "...", "Properties": { "id": "..." } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Inline nodes in detail
|
||||
|
||||
### 6.1 `NodeText` (plain text)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeText", "Data": "plain text" }
|
||||
```
|
||||
|
||||
`Data` **may be omitted** (common as a placeholder zero-width space: `{ "Type": "NodeText" }`).
|
||||
|
||||
### 6.2 `NodeTextMark` (the unified carrier for modern inline formatting)
|
||||
|
||||
In `.sy` files, bold/italic/link/inline-code/block-ref etc. are **almost all** `NodeTextMark`, **not** `NodeStrong`/`NodeEmphasis`/`NodeLink`. `TextMarkType` determines the kind.
|
||||
|
||||
| `TextMarkType` | Meaning | Required fields |
|
||||
|---|---|---|
|
||||
| `text` | plain text | `TextMarkTextContent` |
|
||||
| `strong` | bold | `TextMarkTextContent` |
|
||||
| `em` | italic | `TextMarkTextContent` |
|
||||
| `u` | underline | `TextMarkTextContent` |
|
||||
| `s` | strikethrough (double-tilde `~~`) | `TextMarkTextContent` |
|
||||
| `mark` | highlight | `TextMarkTextContent` |
|
||||
| `sup` / `sub` | super/subscript | `TextMarkTextContent` |
|
||||
| `kbd` | keyboard key | `TextMarkTextContent` |
|
||||
| `code` | inline code | `TextMarkTextContent` |
|
||||
| `tag` | tag `#tag#` | `TextMarkTextContent` |
|
||||
| `a` | hyperlink | `TextMarkAHref`, `TextMarkTextContent` (optional `TextMarkATitle`) |
|
||||
| `block-ref` | block reference | `TextMarkBlockRefID`, `TextMarkBlockRefSubtype`, `TextMarkTextContent` |
|
||||
| `inline-math` | inline math | `TextMarkInlineMathContent` (**no** `TextMarkTextContent`) |
|
||||
| `inline-memo` | inline note | `TextMarkInlineMemoContent`, `TextMarkTextContent` |
|
||||
| `file-annotation-ref` | file-annotation ref | `TextMarkFileAnnotationRefID`, etc. `【inferred】` |
|
||||
|
||||
Samples:
|
||||
|
||||
```json
|
||||
{ "Type": "NodeTextMark", "TextMarkType": "a", "TextMarkAHref": "https://ld246.com", "TextMarkTextContent": "hyperlink" }
|
||||
{ "Type": "NodeTextMark", "TextMarkType": "block-ref", "TextMarkBlockRefID": "20200812220555-lj3enxa", "TextMarkBlockRefSubtype": "s", "TextMarkTextContent": "block ref" }
|
||||
{ "Type": "NodeTextMark", "TextMarkType": "inline-math", "TextMarkInlineMathContent": "a^2 + b^2 = c^2" }
|
||||
{ "Type": "NodeTextMark", "TextMarkType": "inline-memo", "TextMarkInlineMemoContent": "an inline note", "TextMarkTextContent": "note" }
|
||||
```
|
||||
|
||||
- `TextMarkBlockRefSubtype`: `"s"` = static text, `"d"` = dynamic embed.
|
||||
- `TextMarkType` may stack multiple marks separated by spaces, e.g. `"strong em"`.
|
||||
- `TextMarkTextContent` is not present on every type (`inline-math` lacks it).
|
||||
- Strikethrough **supports only double-tilde `~~x~~`**, not single-tilde `~x~` (`SetGFMStrikethrough1(false)`).
|
||||
|
||||
### 6.3 Styled inline text (★ must be paired)
|
||||
|
||||
A `NodeTextMark` carrying color/effects (with `Properties.style`) **must be immediately followed by a** `NodeKramdownSpanIAL`, and the two must share the exact same style text:
|
||||
|
||||
```json
|
||||
{ "Type": "NodeTextMark", "Properties": { "style": "color: var(--b3-font-color1); background-color: var(--b3-font-background1);" },
|
||||
"TextMarkType": "strong", "TextMarkTextContent": "color 1" },
|
||||
{ "Type": "NodeKramdownSpanIAL", "Data": "{: style=\"color: var(--b3-font-color1); background-color: var(--b3-font-background1);\"}" }
|
||||
```
|
||||
|
||||
> When generating styled inline text, these two nodes must appear as a pair, otherwise the kramdown round-trip will drop the style.
|
||||
|
||||
### 6.4 `NodeImage` (seven-part structure)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeImage", "Data": "span", "Children": [
|
||||
{ "Type": "NodeBang" },
|
||||
{ "Type": "NodeOpenBracket" },
|
||||
{ "Type": "NodeLinkText", "Data": "alt text" },
|
||||
{ "Type": "NodeCloseBracket" },
|
||||
{ "Type": "NodeOpenParen" },
|
||||
{ "Type": "NodeLinkDest", "Data": "assets/image-2021.png" },
|
||||
{ "Type": "NodeCloseParen" }
|
||||
] }
|
||||
```
|
||||
|
||||
- The image node itself has `Data` = `"span"`.
|
||||
- `NodeBang`/`NodeOpenBracket`/`NodeCloseBracket`/`NodeOpenParen`/`NodeCloseParen` markers **may omit** `Data`.
|
||||
- Only `NodeLinkText` and `NodeLinkDest` carry `Data`.
|
||||
|
||||
---
|
||||
|
||||
## 7. base64 encoding convention (★ must-read)
|
||||
|
||||
| Field | Encoding | Example |
|
||||
|---|---|---|
|
||||
| `ListData.Marker` | base64 | `Kg==` = `*`, `MS4=` = `1.`, `MQ==` = `1` |
|
||||
| `CodeBlockInfo` | base64 | `Z28=` = `go`, `amF2YQ==` = `java` |
|
||||
| `CodeBlockOpenFence`/`CloseFence` | base64 | `YGBg` = ` ``` ` |
|
||||
| `ListData.BulletChar`/`Delimiter` | **int ASCII codepoint** (**not** base64) | `42` = `*`, `46` = `.` |
|
||||
| `Data` (paragraph text, code content, link, SQL, etc.) | **raw** (not encoded) | `"package main\n..."` |
|
||||
|
||||
> Rule of thumb: **marker fields** like `Marker`/`Fence`/`Info` are base64; **content fields** like `Data`, `TextMarkTextContent`, `TextMarkInlineMathContent` are raw; `BulletChar`/`Delimiter` are int codepoints.
|
||||
|
||||
---
|
||||
|
||||
## 8. Properties (IAL) in full
|
||||
|
||||
A flat `map[string]string`.
|
||||
|
||||
**Document-level (required):** `id`, `title`, `type` (always `"doc"`), `updated`. Optional: `icon` (emoji codepoint hex, e.g. `"1f4f0"`), `title-img` (CSS).
|
||||
|
||||
**Block-level (required):** `id` (= the node's `ID`), `updated`. Optional: `style` (inline CSS), `fold: "1"` (collapsed), `colgroup` (table column widths), arbitrary `custom-*` custom attributes.
|
||||
|
||||
> The authoritative key is **lowercase** `id`. Some legacy imported files also carry a leftover uppercase `ID` — prefer lowercase.
|
||||
|
||||
---
|
||||
|
||||
## 9. Container containment cheat sheet
|
||||
|
||||
| Container | Can contain | Cannot contain |
|
||||
|---|---|---|
|
||||
| `NodeList` | **only** `NodeListItem` | any other block (paragraphs/code blocks/sub-lists must be wrapped in `NodeListItem` first) |
|
||||
| `NodeListItem` | any non-`NodeListItem` block (paragraph/code block/sub-`NodeList`/super block…) | `NodeListItem` (nesting requires another `NodeList`) |
|
||||
| `NodeBlockquote` | any non-`NodeListItem` block + one `NodeBlockquoteMarker` | `NodeListItem` |
|
||||
| `NodeCallout` | any non-`NodeListItem` block | `NodeListItem` |
|
||||
| `NodeSuperBlock` | **any block** (incl. nested super blocks), wrapped by the three markers | none (most permissive) |
|
||||
| `NodeDocument` | any non-`NodeListItem` block | `NodeListItem` |
|
||||
|
||||
> These rules are enforced by Lute's `CanContain` (`ast/node.go:988`). Violations cause parse/render anomalies — AI must obey them when generating.
|
||||
|
||||
---
|
||||
|
||||
## 10. Zero-width space convention
|
||||
|
||||
`.sy` files make heavy use of `` (U+200B) as a separator placeholder. Around images, inline code, tags, kbd, etc., there is **usually a** `NodeText` with `Data` = `` on **each side**, to keep rendering and caret behavior correct. AI should follow this convention when generating such content.
|
||||
|
||||
---
|
||||
|
||||
## 11. Markdown syntax disabled by SiYuan (must not appear in `.sy`)
|
||||
|
||||
SiYuan disables the following syntax via `SetXxx(false)` in `NewLute()` (`kernel/util/lute.go`). The corresponding node types **never** appear in `.sy` files — AI must not generate them:
|
||||
|
||||
| Disabled item | Corresponding node types | Note |
|
||||
|---|---|---|
|
||||
| `SetFootnotes(false)` | `NodeFootnotesDefBlock`/`NodeFootnotesDef`/`NodeFootnotesRef` | footnotes, fully disabled |
|
||||
| `SetToC(false)` | `NodeToC` | `[toc]` table of contents |
|
||||
| `SetIndentCodeBlock(false)` | indented code blocks | only fenced code blocks are supported |
|
||||
| `SetHeadingID(false)` | `NodeHeadingID` | custom heading ID `{#id}` |
|
||||
| `SetSetext(false)` | Setext headings (`===`/`---` underline form) | only ATX-style `#` is supported |
|
||||
| `SetYamlFrontMatter(false)` | `NodeYamlFrontMatter` | YAML front matter |
|
||||
| `SetLinkRef(false)` | `NodeLinkRefDef`/`NodeLinkRefDefBlock` | link reference definitions |
|
||||
| `SetGFMStrikethrough1(false)` | single-tilde strikethrough `~x~` | only double-tilde `~~x~~` is supported |
|
||||
|
||||
---
|
||||
|
||||
## 12. AI write checklist
|
||||
|
||||
When generating a `.sy` that SiYuan can load cleanly, verify item by item:
|
||||
|
||||
1. ☐ Root `Type` = `"NodeDocument"`, `Spec` = `"2"`; root `ID` = filename (without `.sy`) and equals `Properties.id`
|
||||
2. ☐ Root `Properties` contains `id`/`title`/`type:"doc"`/`updated`
|
||||
3. ☐ Every block has a 22-char `ID`; `Properties.id` = `ID`; `Properties.updated` is a valid 14-digit timestamp
|
||||
4. ☐ Inline/marker nodes **do not carry** `ID`
|
||||
5. ☐ Lists are distinguished via `ListData.Typ` (omitted = unordered / `1` = ordered / `3` = task)
|
||||
6. ☐ `NodeList`'s direct children are **only** `NodeListItem`; nested lists wrap another `NodeList`
|
||||
7. ☐ `BulletChar`/`Delimiter` are int codepoints; `Marker` is base64
|
||||
8. ☐ Task checks use uppercase `[X]`, unchecked `[ ]`
|
||||
9. ☐ Code block four parts, math block three parts, embed five parts, super block three parts — structure intact
|
||||
10. ☐ `NodeCodeBlockCode`/`NodeMathBlockContent` are block-level (with ID); surrounding markers are inline (no ID)
|
||||
11. ☐ base64 fields are encoded; content fields stay raw
|
||||
12. ☐ Prefer `NodeTextMark` for inline formatting over legacy `NodeStrong`/`NodeEmphasis`/`NodeLink`
|
||||
13. ☐ A styled `TextMark` must be followed by a paired `NodeKramdownSpanIAL`
|
||||
14. ☐ HTML/IFrame/Video/Audio/AttributeView are leaves with no `Children` (content in `Data` or type-specific fields)
|
||||
15. ☐ Do not fabricate `AttributeViewID`/`block-ref` target IDs (they must point to real blocks/AVs)
|
||||
16. ☐ Do not generate disabled types (footnotes/ToC/YAML/LinkRef/HeadingID, etc. — see §11)
|
||||
|
||||
---
|
||||
|
||||
## 13. Pitfalls and common mistakes
|
||||
|
||||
| ❌ Wrong | ✅ Correct |
|
||||
|---|---|
|
||||
| Assuming every node has `Data` | `Data` may be omitted; marker nodes often lack it |
|
||||
| Inline node carries `ID` | Inline/marker nodes have no `ID` |
|
||||
| Using legacy nodes like `NodeStrong`/`NodeLink` | Use `NodeTextMark` + `TextMarkType` |
|
||||
| `ListData.Typ` only accepts `1` | omitted = unordered, `1` = ordered, `3` = task |
|
||||
| Treating `BulletChar` as base64 | It's an int ASCII codepoint (`42`) |
|
||||
| Writing task checks lowercase `[x]` | Uppercase `[X]` |
|
||||
| Styled `TextMark` without the IAL | Must pair with `NodeKramdownSpanIAL` |
|
||||
| Adding `Children` to `NodeAttributeView` | It's a leaf — use `AttributeViewID`/`AttributeViewType` |
|
||||
| Changing `ID` without syncing `Properties.id` | The two must match |
|
||||
| `inline-math` carrying `TextMarkTextContent` | It only has `TextMarkInlineMathContent` |
|
||||
| Fabricating block-ref / AV target IDs | Targets must really exist |
|
||||
| Hanging a paragraph directly under `NodeList` | `NodeList` can only contain `NodeListItem` — wrap first |
|
||||
| Generating footnotes/ToC/YAML, etc. | SiYuan disables these; they never appear in `.sy` |
|
||||
|
||||
---
|
||||
|
||||
## 14. Minimal writable document template
|
||||
|
||||
```json
|
||||
{
|
||||
"ID": "20260628120000-abc1234",
|
||||
"Spec": "2",
|
||||
"Type": "NodeDocument",
|
||||
"Properties": {
|
||||
"id": "20260628120000-abc1234",
|
||||
"title": "New doc",
|
||||
"type": "doc",
|
||||
"updated": "20260628120000"
|
||||
},
|
||||
"Children": [
|
||||
{
|
||||
"Type": "NodeHeading", "ID": "20260628120001-def5678", "HeadingLevel": 2,
|
||||
"Properties": { "id": "20260628120001-def5678", "updated": "20260628120001" },
|
||||
"Children": [ { "Type": "NodeText", "Data": "Heading" } ]
|
||||
},
|
||||
{
|
||||
"Type": "NodeParagraph", "ID": "20260628120002-ghi9012",
|
||||
"Properties": { "id": "20260628120002-ghi9012", "updated": "20260628120002" },
|
||||
"Children": [
|
||||
{ "Type": "NodeText", "Data": "Body with " },
|
||||
{ "Type": "NodeTextMark", "TextMarkType": "strong", "TextMarkTextContent": "bold" },
|
||||
{ "Type": "NodeText", "Data": "." }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix: verification sources
|
||||
|
||||
- Sample 1: `app/guide/.../20200825162036-4dx365o.sy` (formatting elements — covers nearly all block types)
|
||||
- Sample 2: `app/guide/.../20200905090211-2vixtlf.sy` (block types — incl. compact lists, AttributeView)
|
||||
- Node-type constants and serialization logic: `lute/ast/node.go`, `lute/render/json_renderer.go`, `dataparser/sy.go`
|
||||
- List containment check: `lute/ast/node.go:988` (`CanContain`)
|
||||
- Disabled-syntax config: `kernel/util/lute.go:51` (`NewLute`)
|
||||
- Fields marked `【inferred】` (e.g. the sub-fields of `file-annotation-ref`) should be re-verified against a real sample before generation.
|
||||
@@ -0,0 +1,592 @@
|
||||
# SiYuan `.sy` 文件 JSON 结构规范 —— AI 读写指南
|
||||
|
||||
> 版本基准:Spec `2`(当前所有文件)。
|
||||
> 核验样本:`20200825162036-4dx365o.sy`(排版元素)、`20200905090211-2vixtlf.sy`(内容块类型)。
|
||||
> 本文档所有结论均基于真实样本与 Lute / 思源内核代码核验;标注 `【推断】` 的字段表示未在样本中直接出现,建议生成前再用真实样本核验。
|
||||
|
||||
## 0. 一句话本质
|
||||
|
||||
`.sy` = 一个 Lute AST 树序列化成的 JSON。根节点恒为 `NodeDocument`,正文是其 `Children` 数组递归嵌套的节点树。没有外部 schema,全部状态在树里。
|
||||
|
||||
---
|
||||
|
||||
## 0.5 何时直接读写 `.sy`(优先级)
|
||||
|
||||
思源提供了 **HTTP API、MCP、CLI** 三条官方路径来修改数据。**默认应优先使用它们**,因为内核会负责 AST 序列化、块 ID 分配,以及两套索引的同步——块树索引(`blocktree.db`,块 ID → 文件路径映射,块引用/面包屑依赖它)和全文搜索索引(`siyuan.db` + FTS5)。直接改盘绕过了这些逻辑,容易导致索引不一致。
|
||||
|
||||
**仅当官方路径不便时,才直接以 JSON 读写 `.sy`**。适用场景:
|
||||
- 批量离线迁移(冷初始化工作区、外部数据导入)
|
||||
- 只读的内容统计、分析、自定义导出/格式转换
|
||||
- 修复底层结构问题(遗留文件、非法节点)
|
||||
- 程序化生成模板/脚手架
|
||||
|
||||
四条路径的分工:
|
||||
|
||||
| 路径 | 定位 | 修改能力 |
|
||||
|---|---|---|
|
||||
| **HTTP API** | 运行时在线操作 | 最全,文档/块的增删改查(`filetree/*`、`block/*`、`transactions`) |
|
||||
| **MCP** | LLM 工具集 | AI agent 在线操作文档的子集 |
|
||||
| **CLI** | 批处理 / 运维 | 导入、导出、同步、SQL 等命令行任务 |
|
||||
| **直接读写 `.sy`** | 本规范覆盖范围 | 离线、批量、底层结构操作 |
|
||||
|
||||
> ⚠️ 直接改盘后,通常需要触发一次"重建索引"才能让搜索/块引用生效。若思源正在运行,更稳妥的是走 HTTP API,由内核负责序列化与索引同步。
|
||||
|
||||
---
|
||||
|
||||
## 1. 整体结构
|
||||
|
||||
```json
|
||||
{
|
||||
"ID": "20200825162036-4dx365o",
|
||||
"Spec": "2",
|
||||
"Type": "NodeDocument",
|
||||
"Properties": {
|
||||
"icon": "1f4f0",
|
||||
"id": "20200825162036-4dx365o",
|
||||
"title": "排版元素",
|
||||
"type": "doc",
|
||||
"updated": "20260616224229"
|
||||
},
|
||||
"Children": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
| 顶层键 | 必有 | 说明 |
|
||||
|---|---|---|
|
||||
| `ID` | ✅ | 文档块 ID,**等于文件名去掉 `.sy`**(不是随机生成) |
|
||||
| `Spec` | ✅ | 恒 `"2"` |
|
||||
| `Type` | ✅ | 恒 `"NodeDocument"` |
|
||||
| `Properties` | ✅ | 文档级 IAL,见 §8 |
|
||||
| `Children` | ✅ | 正文子块数组,至少含一个块 |
|
||||
|
||||
> ⚠️ 文件路径与根 ID 严格对应:`data/<box>/<...>/<根ID>.sy`。改根 ID 等于改文件名,AI 不要随意改根 ID。
|
||||
|
||||
---
|
||||
|
||||
## 2. 通用字段语义(每个节点都适用)
|
||||
|
||||
| 字段 | 类型 | 出现条件 | 语义 |
|
||||
|---|---|---|---|
|
||||
| `Type` | string | **所有节点必有** | 类型判别字段,如 `"NodeParagraph"` |
|
||||
| `ID` | string | 仅 block 节点 | 22 字符块 ID;内联/标记节点**不带** |
|
||||
| `Data` | string | 部分 | 文本/HTML/markdown 原文;**可省略**(不能假设必有) |
|
||||
| `Properties` | object | 多数 block | IAL,`map[string]string` |
|
||||
| `Children` | array | 容器/可嵌套节点 | 子节点数组 |
|
||||
| 类型专属字段 | - | 按类型 | 如 `HeadingLevel`、`ListData`、`TextMarkType`、`AttributeViewID` |
|
||||
|
||||
**核心判别规则**:是否有 `ID` 决定是否是 block。有 `ID` ⇒ block(其 `Properties.id` 必须等于 `ID`);无 `ID` ⇒ 内联/标记节点。
|
||||
|
||||
---
|
||||
|
||||
## 3. ID 与时间戳规则
|
||||
|
||||
- **ID 格式**:`YYYYMMDDHHMMSS-xxxxxxx` = 14 位时间戳 + `-` + 7 位随机 `[a-z0-9]`。例:`20210104091228-ttcj9nm`。
|
||||
- **根 ID** 来自文件名,**不**重新生成。
|
||||
- **子块 ID** 用上述算法生成。
|
||||
- `Properties.updated` 用 14 位时间戳,语义"最后更新时间"。
|
||||
- 改任何 block 的 `ID`,必须**同步** `Properties.id`。`Properties.updated` 建议同步刷新为当前时间。
|
||||
|
||||
---
|
||||
|
||||
## 4. 节点类型目录
|
||||
|
||||
### Block 节点(有 ID)
|
||||
|
||||
**叶子 block**:`NodeParagraph`、`NodeHeading`、`NodeThematicBreak`、`NodeHTMLBlock`、`NodeCodeBlock`、`NodeMathBlock`、`NodeTable`、`NodeBlockQueryEmbed`、`NodeAttributeView`、`NodeIFrame`、`NodeVideo`、`NodeAudio`、`NodeWidget`、`NodeCustomBlock`、`NodeGitConflict`
|
||||
|
||||
**容器 block**:`NodeList`、`NodeListItem`、`NodeBlockquote`、`NodeCallout`、`NodeSuperBlock`
|
||||
|
||||
**块级内容子节点**(位于块内部,也带 ID):`NodeCodeBlockCode`、`NodeMathBlockContent`
|
||||
|
||||
### 内联/标记节点(无 ID)
|
||||
|
||||
`NodeText`、`NodeTextMark`、`NodeImage`、`NodeKramdownSpanIAL`、`NodeHeadingC8hMarker`、`NodeBlockquoteMarker`、`NodeTaskListItemMarker`、`NodeBang`、`NodeOpenBracket`、`NodeCloseBracket`、`NodeOpenParen`、`NodeCloseParen`、`NodeLinkText`、`NodeLinkDest`、`NodeCodeBlockFenceOpenMarker`、`NodeCodeBlockFenceInfoMarker`、`NodeCodeBlockFenceCloseMarker`、`NodeMathBlockOpenMarker`、`NodeMathBlockCloseMarker`、`NodeSuperBlockOpenMarker`、`NodeSuperBlockLayoutMarker`、`NodeSuperBlockCloseMarker`、`NodeOpenBrace`、`NodeCloseBrace`、`NodeBlockQueryEmbedScript`、`NodeTableHead`、`NodeTableRow`、`NodeTableCell`
|
||||
|
||||
> 思源禁用的类型(脚注 / ToC / YAML / LinkRef / HeadingID 等)见 §11,不应出现在 `.sy` 中,故未列入目录。
|
||||
|
||||
---
|
||||
|
||||
## 5. 各 Block 类型详解 + 可复制样例
|
||||
|
||||
### 5.1 段落
|
||||
|
||||
```json
|
||||
{ "Type": "NodeParagraph", "ID": "...", "Properties": { "id": "...", "updated": "..." },
|
||||
"Children": [ { "Type": "NodeText", "Data": "这里是一个示例段落。" } ] }
|
||||
```
|
||||
|
||||
### 5.2 标题
|
||||
|
||||
```json
|
||||
{ "Type": "NodeHeading", "ID": "...", "HeadingLevel": 2,
|
||||
"Properties": { "id": "...", "updated": "..." },
|
||||
"Children": [ { "Type": "NodeText", "Data": "标题块" } ] }
|
||||
```
|
||||
|
||||
- `HeadingLevel` 取值 `1`–`6`。
|
||||
- `NodeHeadingC8hMarker`(`Data` 如 `"## "`)**可选**,有无都合法。建议生成时**省略**它,更简洁。
|
||||
- SiYuan 建议**正文顶层用二级标题**,不要用一级。
|
||||
|
||||
### 5.3 列表(关键:用 `ListData.Typ` 区分类型)
|
||||
|
||||
> **★ 列表的硬性结构约束**:`NodeList` 的直接子节点**只能**是 `NodeListItem`(Lute 在 `CanContain` 中强制校验,`ast/node.go:993` `return NodeListItem == nodeType`)。段落、代码块、子列表等任何其他块都**不能**直接挂在 `NodeList` 下,必须先包一层 `NodeListItem`。
|
||||
|
||||
```
|
||||
✅ 正确 ❌ 错误
|
||||
NodeList NodeList
|
||||
└─ NodeListItem ├─ NodeParagraph ← 非法
|
||||
└─ NodeParagraph └─ NodeCodeBlock ← 非法
|
||||
```
|
||||
|
||||
**嵌套列表**的正确写法是再套一层 `NodeList`(`NodeListItem` 走默认 `CanContain` 分支,不能直接含另一个 `NodeListItem`):
|
||||
|
||||
```
|
||||
✅ 正确 ❌ 错误
|
||||
NodeList NodeList
|
||||
└─ NodeListItem └─ NodeListItem
|
||||
├─ NodeParagraph ├─ NodeParagraph
|
||||
└─ NodeList ← 子列表 └─ NodeListItem ← 非法
|
||||
└─ NodeListItem
|
||||
└─ NodeParagraph
|
||||
```
|
||||
|
||||
**无序列表**(`Typ` 缺省):
|
||||
|
||||
```json
|
||||
{ "Type": "NodeList", "ID": "...", "ListData": {},
|
||||
"Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeListItem", "ID": "...",
|
||||
"ListData": { "BulletChar": 42, "Marker": "Kg==" },
|
||||
"Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeParagraph", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [ { "Type": "NodeText", "Data": "列表项一" } ] }
|
||||
] }
|
||||
] }
|
||||
```
|
||||
|
||||
**有序列表**(`Typ: 1`):
|
||||
|
||||
```json
|
||||
{ "Type": "NodeListItem", "ID": "...", "Data": "1",
|
||||
"ListData": { "Typ": 1, "Tight": true, "Start": 1, "Delimiter": 46, "Padding": 3, "Marker": "MQ==", "Num": 1 },
|
||||
"Properties": { "id": "..." }, "Children": [ ... ] }
|
||||
```
|
||||
|
||||
**任务列表**(`Typ: 3`),子节点首项是 `NodeTaskListItemMarker`:
|
||||
|
||||
```json
|
||||
{ "Type": "NodeListItem", "ID": "...",
|
||||
"ListData": { "Typ": 3, "Tight": true, "BulletChar": 45, "Padding": 2, "Checked": true, "Marker": "LQ==", "Num": -1 },
|
||||
"Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeTaskListItemMarker", "Data": "[X]", "TaskListItemChecked": true },
|
||||
{ "Type": "NodeParagraph", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [ { "Type": "NodeText", "Data": "待办一" } ] }
|
||||
] }
|
||||
```
|
||||
|
||||
### 5.4 `ListData` 字段全解(★ 最易踩坑)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `Typ` | int | **列表类型判别**:缺省=无序,`1`=有序,`3`=任务 |
|
||||
| `Tight` | bool | 紧凑态(无空行);可选 |
|
||||
| `BulletChar` | int | 无序/任务列表的项目符号 **ASCII 码点**(`42`=`*`,`45`=`-`) |
|
||||
| `Delimiter` | int | 有序列表分隔符 ASCII 码点(`46`=`.`) |
|
||||
| `Start` | int | 有序列表起始编号 |
|
||||
| `Num` | int | 该项序号;**无序/任务列表恒为 `-1`** |
|
||||
| `Padding` | int | 缩进填充数;可选 |
|
||||
| `Checked` | bool | 任务列表项是否勾选(列表级聚合) |
|
||||
| `Marker` | string(base64) | 标记符原文的 **base64**;可能含分隔符(`"MS4="`=`1.`)也可能不含(`"MQ=="`=`1`) |
|
||||
|
||||
> 关键区分:**`BulletChar`/`Delimiter` 是 int(ASCII 码点)**;**`Marker` 是 base64 字符串**。二者容易混淆。
|
||||
|
||||
### 5.5 任务标记
|
||||
|
||||
```json
|
||||
// 已勾选
|
||||
{ "Type": "NodeTaskListItemMarker", "Data": "[X]", "TaskListItemChecked": true }
|
||||
// 未勾选
|
||||
{ "Type": "NodeTaskListItemMarker", "Data": "[ ]" }
|
||||
```
|
||||
|
||||
> 勾选态 `Data` 是大写 `[X]`,**不是** `[x]`。未勾选可省略 `TaskListItemChecked`(缺省即 false)。
|
||||
|
||||
### 5.6 引述块
|
||||
|
||||
```json
|
||||
{ "Type": "NodeBlockquote", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeBlockquoteMarker", "Data": "> " },
|
||||
{ "Type": "NodeParagraph", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [ { "Type": "NodeText", "Data": "引述内容" } ] }
|
||||
] }
|
||||
```
|
||||
|
||||
> `NodeBlockquoteMarker.Data` 可以是 `">"` 或 `"> "`,都合法。
|
||||
|
||||
### 5.7 提示块(Callout / GFM Alert)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeCallout", "ID": "...",
|
||||
"CalloutType": "NOTE", "CalloutTitle": "Note", "CalloutIcon": "✏️",
|
||||
"Properties": { "id": "...", "updated": "..." },
|
||||
"Children": [ { "Type": "NodeParagraph", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [ { "Type": "NodeText", "Data": "提示内容" } ] } ] }
|
||||
```
|
||||
|
||||
| `CalloutType` | `CalloutTitle` | `CalloutIcon` |
|
||||
|---|---|---|
|
||||
| `NOTE` | `Note` | `✏️` |
|
||||
| `TIP` | `Tip` | `💡` |
|
||||
| `IMPORTANT` | `Important` | `❗` |
|
||||
| `WARNING` | `Warning` | `⚠️` |
|
||||
| `CAUTION` | `Caution` | `🚨` |
|
||||
|
||||
> `CalloutIcon` 是**直接 emoji 字符**,不是 base64、不是码点。
|
||||
|
||||
### 5.8 超级块(可嵌套,三段结构)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeSuperBlock", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeSuperBlockOpenMarker" },
|
||||
{ "Type": "NodeSuperBlockLayoutMarker", "Data": "col" },
|
||||
{ "Type": "NodeSuperBlock", "ID": "...", "Properties": { "id": "..." }, "Children": [ ... 内嵌超级块,Data 为 "row" ... ] },
|
||||
{ "Type": "NodeSuperBlockCloseMarker" }
|
||||
] }
|
||||
```
|
||||
|
||||
> `NodeSuperBlockLayoutMarker.Data` 只能是 `"row"`(横向)或 `"col"`(纵向)。超级块可嵌套,且是唯一能容纳任意块(含嵌套自身)的容器。
|
||||
|
||||
### 5.9 嵌入块(五段结构 `{{ ... }}`)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeBlockQueryEmbed", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeOpenBrace" },
|
||||
{ "Type": "NodeOpenBrace" },
|
||||
{ "Type": "NodeBlockQueryEmbedScript", "Data": "select * from blocks where id='20210428212840-8rqwn5o'" },
|
||||
{ "Type": "NodeCloseBrace" },
|
||||
{ "Type": "NodeCloseBrace" }
|
||||
] }
|
||||
```
|
||||
|
||||
### 5.10 代码块(四段结构,仅围栏式)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeCodeBlock", "ID": "...", "IsFencedCodeBlock": true,
|
||||
"CodeBlockFenceChar": 96, "CodeBlockFenceLen": 3,
|
||||
"CodeBlockOpenFence": "YGBg", "CodeBlockInfo": "Z28=", "CodeBlockCloseFence": "YGBg",
|
||||
"Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeCodeBlockFenceOpenMarker", "Data": "```", "CodeBlockFenceLen": 3 },
|
||||
{ "Type": "NodeCodeBlockFenceInfoMarker", "CodeBlockInfo": "Z28=" },
|
||||
{ "Type": "NodeCodeBlockCode", "ID": "...", "Data": "package main\n...\n", "Properties": { "id": "..." } },
|
||||
{ "Type": "NodeCodeBlockFenceCloseMarker", "Data": "```", "CodeBlockFenceLen": 3 }
|
||||
] }
|
||||
```
|
||||
|
||||
要点:
|
||||
- `NodeCodeBlockCode` 是**块级**(有 `ID`、`Properties`),内容放 `Data`(原始文本,`\n` 转义)。
|
||||
- 外围 marker 是内联(无 ID)。
|
||||
- `CodeBlockInfo` 是**语言名的 base64**(`"Z28="`=`go`)。三个 fence 字段(`CodeBlockOpenFence`/`CloseFence` 都是 `"YGBg"`=` ``` `)和 `CodeBlockFenceChar`/`FenceLen` 是**可选**的(简写时可省略,只用 `IsFencedCodeBlock:true`)。
|
||||
- 思源**不支持缩进式代码块**(`SetIndentCodeBlock(false)`),所有代码块都是围栏式。
|
||||
|
||||
### 5.11 数学块(三段结构)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeMathBlock", "ID": "...", "Properties": { "id": "..." },
|
||||
"Children": [
|
||||
{ "Type": "NodeMathBlockOpenMarker" },
|
||||
{ "Type": "NodeMathBlockContent", "ID": "...", "Data": "a^2 + b^2 = c^2", "Properties": { "id": "..." } },
|
||||
{ "Type": "NodeMathBlockCloseMarker" }
|
||||
] }
|
||||
```
|
||||
|
||||
### 5.12 HTML / IFrame / Video / Audio 块(叶子,内容在顶层 `Data`)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeHTMLBlock", "ID": "...", "Data": "<div>\n<ruby>你<rt>nǐ</rt>...</div>", "Properties": { "id": "..." } }
|
||||
{ "Type": "NodeIFrame", "ID": "...", "Data": "<iframe src=\"...\"></iframe>", "Properties": { "id": "..." } }
|
||||
{ "Type": "NodeVideo", "ID": "...", "Data": "<video controls src=\"assets/x.mp4\"></video>", "Properties": { "id": "..." } }
|
||||
{ "Type": "NodeAudio", "ID": "...", "Data": "<audio controls src=\"assets/x.wav\"></audio>", "Properties": { "id": "..." } }
|
||||
```
|
||||
|
||||
> 这四种**没有 Children**,HTML 内容(JSON 转义后)直接放顶层 `Data`。
|
||||
|
||||
### 5.13 表格
|
||||
|
||||
```json
|
||||
{ "Type": "NodeTable", "ID": "...", "TableAligns": [0, 0, 0],
|
||||
"Properties": { "id": "...", "colgroup": "||" },
|
||||
"Children": [
|
||||
{ "Type": "NodeTableHead", "Data": "thead", "Children": [
|
||||
{ "Type": "NodeTableRow", "Data": "tr", "Children": [
|
||||
{ "Type": "NodeTableCell", "Data": "th", "Children": [ { "Type": "NodeText", "Data": "表头" } ] }
|
||||
] }
|
||||
] },
|
||||
{ "Type": "NodeTableRow", "Data": "tr", "Children": [
|
||||
{ "Type": "NodeTableCell", "Data": "td", "Children": [ { "Type": "NodeText", "Data": "单元格" } ] }
|
||||
] }
|
||||
] }
|
||||
```
|
||||
|
||||
- 嵌套层级固定:`NodeTable > NodeTableHead/NodeTableRow > NodeTableCell > 内联`。
|
||||
- `TableAligns`:每列对齐值的 int 数组,`0`=默认/左。
|
||||
- `Data`(`thead`/`tr`/`th`/`td`)在紧凑文件里**可省略**。
|
||||
- 列宽用 `Properties.colgroup`(`|` 分隔)记录。
|
||||
|
||||
### 5.14 数据库块(AttributeView,叶子)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeAttributeView", "ID": "...",
|
||||
"Properties": { "custom-sy-av-view": "20251230141609-lcme2fh", "id": "...", "updated": "..." },
|
||||
"AttributeViewID": "20251230141609-2kvghrg",
|
||||
"AttributeViewType": "table" }
|
||||
```
|
||||
|
||||
- **没有 Children**。
|
||||
- `AttributeViewID` 指向 AV 表数据(存在单独的 `.json`,**不要**由 AI 凭空构造 ID)。
|
||||
- `AttributeViewType`:`table` / `kanban` / `gallery` 等。
|
||||
- `custom-sy-av-view` 记录当前视图 ID。
|
||||
|
||||
> 建议 AI **不要创建新的 AttributeView 块**,因为表数据不在 `.sy` 里,需配套文件。
|
||||
|
||||
### 5.15 分隔线
|
||||
|
||||
```json
|
||||
{ "Type": "NodeThematicBreak", "ID": "...", "Properties": { "id": "..." } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 内联节点详解
|
||||
|
||||
### 6.1 `NodeText`(纯文本)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeText", "Data": "普通文本" }
|
||||
```
|
||||
|
||||
`Data` **可省略**(用作占位零宽空格时常见 `{ "Type": "NodeText" }`)。
|
||||
|
||||
### 6.2 `NodeTextMark`(现代内联格式的统一载体)
|
||||
|
||||
`.sy` 里加粗/斜体/链接/行内代码/块引用等**几乎全部**用 `NodeTextMark`,而**不是** `NodeStrong`/`NodeEmphasis`/`NodeLink`。`TextMarkType` 决定类型。
|
||||
|
||||
| `TextMarkType` | 含义 | 必带字段 |
|
||||
|---|---|---|
|
||||
| `text` | 纯文本 | `TextMarkTextContent` |
|
||||
| `strong` | 加粗 | `TextMarkTextContent` |
|
||||
| `em` | 倾斜 | `TextMarkTextContent` |
|
||||
| `u` | 下划线 | `TextMarkTextContent` |
|
||||
| `s` | 删除线(双波浪 `~~`) | `TextMarkTextContent` |
|
||||
| `mark` | 高亮 | `TextMarkTextContent` |
|
||||
| `sup` / `sub` | 上/下标 | `TextMarkTextContent` |
|
||||
| `kbd` | 键盘键 | `TextMarkTextContent` |
|
||||
| `code` | 行内代码 | `TextMarkTextContent` |
|
||||
| `tag` | 标签 `#tag#` | `TextMarkTextContent` |
|
||||
| `a` | 超链接 | `TextMarkAHref`、`TextMarkTextContent`(可选 `TextMarkATitle`) |
|
||||
| `block-ref` | 块引用 | `TextMarkBlockRefID`、`TextMarkBlockRefSubtype`、`TextMarkTextContent` |
|
||||
| `inline-math` | 行内公式 | `TextMarkInlineMathContent`(**无** `TextMarkTextContent`) |
|
||||
| `inline-memo` | 行级备注 | `TextMarkInlineMemoContent`、`TextMarkTextContent` |
|
||||
| `file-annotation-ref` | 文件注释引用 | `TextMarkFileAnnotationRefID` 等 `【推断】` |
|
||||
|
||||
样例:
|
||||
|
||||
```json
|
||||
{ "Type": "NodeTextMark", "TextMarkType": "a", "TextMarkAHref": "https://ld246.com", "TextMarkTextContent": "超链接" }
|
||||
{ "Type": "NodeTextMark", "TextMarkType": "block-ref", "TextMarkBlockRefID": "20200812220555-lj3enxa", "TextMarkBlockRefSubtype": "s", "TextMarkTextContent": "块引用" }
|
||||
{ "Type": "NodeTextMark", "TextMarkType": "inline-math", "TextMarkInlineMathContent": "a^2 + b^2 = c^2" }
|
||||
{ "Type": "NodeTextMark", "TextMarkType": "inline-memo", "TextMarkInlineMemoContent": "这是一个行级备注", "TextMarkTextContent": "备注" }
|
||||
```
|
||||
|
||||
- `TextMarkBlockRefSubtype`:`"s"`=静态文本,`"d"`=动态嵌入。
|
||||
- `TextMarkType` 可空格叠加多标记,如 `"strong em"`。
|
||||
- `TextMarkTextContent` 不是所有类型都有(`inline-math` 就没有)。
|
||||
- 删除线**仅支持双波浪 `~~x~~`**,不支持单波浪 `~x~`(`SetGFMStrikethrough1(false)`)。
|
||||
|
||||
### 6.3 带样式的内联文本(★ 必须成对)
|
||||
|
||||
带颜色/特效的 `NodeTextMark`(带 `Properties.style`)**后面必须紧跟一个** `NodeKramdownSpanIAL`,且二者 style 文本一致:
|
||||
|
||||
```json
|
||||
{ "Type": "NodeTextMark", "Properties": { "style": "color: var(--b3-font-color1); background-color: var(--b3-font-background1);" },
|
||||
"TextMarkType": "strong", "TextMarkTextContent": "颜色 1" },
|
||||
{ "Type": "NodeKramdownSpanIAL", "Data": "{: style=\"color: var(--b3-font-color1); background-color: var(--b3-font-background1);\"}" }
|
||||
```
|
||||
|
||||
> AI 生成带样式的内联文本时,这两节点必须成对出现,否则 kramdown 往返会丢样式。
|
||||
|
||||
### 6.4 `NodeImage`(七段子结构)
|
||||
|
||||
```json
|
||||
{ "Type": "NodeImage", "Data": "span", "Children": [
|
||||
{ "Type": "NodeBang" },
|
||||
{ "Type": "NodeOpenBracket" },
|
||||
{ "Type": "NodeLinkText", "Data": "alt 文本" },
|
||||
{ "Type": "NodeCloseBracket" },
|
||||
{ "Type": "NodeOpenParen" },
|
||||
{ "Type": "NodeLinkDest", "Data": "assets/image-2021.png" },
|
||||
{ "Type": "NodeCloseParen" }
|
||||
] }
|
||||
```
|
||||
|
||||
- 图片节点本身 `Data` 是 `"span"`。
|
||||
- `NodeBang`/`NodeOpenBracket`/`NodeCloseBracket`/`NodeOpenParen`/`NodeCloseParen` 这些 marker 的 `Data` **可省略**。
|
||||
- 只有 `NodeLinkText`、`NodeLinkDest` 带 `Data`。
|
||||
|
||||
---
|
||||
|
||||
## 7. base64 编码约定(★ 必读)
|
||||
|
||||
| 字段 | 编码 | 例 |
|
||||
|---|---|---|
|
||||
| `ListData.Marker` | base64 | `Kg==`=`*`,`MS4=`=`1.`,`MQ==`=`1` |
|
||||
| `CodeBlockInfo` | base64 | `Z28=`=`go`,`amF2YQ==`=`java` |
|
||||
| `CodeBlockOpenFence`/`CloseFence` | base64 | `YGBg`=` ``` ` |
|
||||
| `ListData.BulletChar`/`Delimiter` | **int ASCII 码点**(**不是** base64) | `42`=`*`,`46`=`.` |
|
||||
| `Data`(段落文本、代码内容、链接、SQL 等) | **原文**(不编码) | `"package main\n..."` |
|
||||
|
||||
> 判别规则:`Marker`/`Fence`/`Info` 这类**标记符字段**是 base64;`Data`、`TextMarkTextContent`、`TextMarkInlineMathContent` 等内容字段是原文;`BulletChar`/`Delimiter` 是 int 码点。
|
||||
|
||||
---
|
||||
|
||||
## 8. Properties(IAL)全解
|
||||
|
||||
扁平 `map[string]string`。
|
||||
|
||||
**文档级必有**:`id`、`title`、`type`(恒 `"doc"`)、`updated`。可选:`icon`(emoji 码点十六进制,如 `"1f4f0"`)、`title-img`(CSS)。
|
||||
|
||||
**Block 级必有**:`id`(= 节点 `ID`)、`updated`。可选:`style`(行内 CSS)、`fold: "1"`(折叠态)、`colgroup`(表格列宽)、任意 `custom-*` 自定义属性。
|
||||
|
||||
> 权威键是**小写** `id`。某些旧导入文件里同时有遗留的大写 `ID`,以小写为准。
|
||||
|
||||
---
|
||||
|
||||
## 9. 容器容纳规则速查
|
||||
|
||||
| 容器 | 可含 | 不可含 |
|
||||
|---|---|---|
|
||||
| `NodeList` | **仅** `NodeListItem` | 任何其他块(段落/代码块/子列表都必须先套 `NodeListItem`) |
|
||||
| `NodeListItem` | 任意非 `NodeListItem` 块(段落/代码块/子 `NodeList`/超级块…) | `NodeListItem`(嵌套要再套 `NodeList`) |
|
||||
| `NodeBlockquote` | 任意非 `NodeListItem` 块 + 一个 `NodeBlockquoteMarker` | `NodeListItem` |
|
||||
| `NodeCallout` | 任意非 `NodeListItem` 块 | `NodeListItem` |
|
||||
| `NodeSuperBlock` | **任意块**(含嵌套超级块),三段 marker 包裹 | 无(最宽松) |
|
||||
| `NodeDocument` | 任意非 `NodeListItem` 块 | `NodeListItem` |
|
||||
|
||||
> 这些规则由 Lute 的 `CanContain`(`ast/node.go:988`)强制校验,违反会导致解析/渲染异常,AI 生成时必须遵守。
|
||||
|
||||
---
|
||||
|
||||
## 10. 零宽空格约定
|
||||
|
||||
`.sy` 大量使用 ``(U+200B)作分隔占位。图片、行内代码、标签、kbd 等内联元素**两侧通常各有一个** `Data` 为 `` 的 `NodeText`,保证渲染与光标行为。AI 生成这类内容时建议遵循该约定。
|
||||
|
||||
---
|
||||
|
||||
## 11. 思源禁用的 Markdown 语法(不应出现在 `.sy` 中)
|
||||
|
||||
思源在 `kernel/util/lute.go` 的 `NewLute()` 中通过 `SetXxx(false)` 禁用了以下语法,对应节点类型**永远不会**出现在 `.sy` 文件里,AI 不要生成:
|
||||
|
||||
| 禁用项 | 对应节点类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `SetFootnotes(false)` | `NodeFootnotesDefBlock`/`NodeFootnotesDef`/`NodeFootnotesRef` | 脚注,全禁 |
|
||||
| `SetToC(false)` | `NodeToC` | `[toc]` 目录 |
|
||||
| `SetIndentCodeBlock(false)` | 缩进式代码块 | 仅支持围栏代码块 |
|
||||
| `SetHeadingID(false)` | `NodeHeadingID` | 自定义标题 ID `{#id}` |
|
||||
| `SetSetext(false)` | Setext 标题(`===`/`---` 下划线式) | 仅支持 ATX 风格 `#` |
|
||||
| `SetYamlFrontMatter(false)` | `NodeYamlFrontMatter` | YAML 前置元数据 |
|
||||
| `SetLinkRef(false)` | `NodeLinkRefDef`/`NodeLinkRefDefBlock` | 链接引用定义 |
|
||||
| `SetGFMStrikethrough1(false)` | 单波浪线删除线 `~x~` | 仅支持双波浪 `~~x~~` |
|
||||
|
||||
---
|
||||
|
||||
## 12. AI 写入检查清单
|
||||
|
||||
生成一份能被 SiYuan 正常加载的 `.sy`,逐条核对:
|
||||
|
||||
1. ☐ 根 `Type`=`"NodeDocument"`、`Spec`=`"2"`,根 `ID`=文件名(去 `.sy`)且 = `Properties.id`
|
||||
2. ☐ 根 `Properties` 含 `id`/`title`/`type:"doc"`/`updated`
|
||||
3. ☐ 每个 block 有 22 字符 `ID`,且 `Properties.id` = `ID`,`Properties.updated` 为合法 14 位时间戳
|
||||
4. ☐ 内联/标记节点**不带** `ID`
|
||||
5. ☐ 列表用 `ListData.Typ` 区分(缺省=无序/`1`=有序/`3`=任务)
|
||||
6. ☐ `NodeList` 直接子节点**只能是** `NodeListItem`;嵌套列表要再套一层 `NodeList`
|
||||
7. ☐ `BulletChar`/`Delimiter` 用 int 码点;`Marker` 用 base64
|
||||
8. ☐ 任务勾选用 `[X]`(大写),未勾选 `[ ]`
|
||||
9. ☐ 代码块四段、数学块三段、嵌入块五段、超级块三段——结构完整
|
||||
10. ☐ `NodeCodeBlockCode`/`NodeMathBlockContent` 是块级(带 ID);外围 marker 是内联(无 ID)
|
||||
11. ☐ base64 字段已编码;内容字段保持原文
|
||||
12. ☐ 内联格式优先用 `NodeTextMark`,不用 `NodeStrong`/`NodeEmphasis`/`NodeLink`
|
||||
13. ☐ 带样式的 TextMark 后必须跟配对的 `NodeKramdownSpanIAL`
|
||||
14. ☐ HTML/IFrame/Video/Audio/AttributeView 是叶子,无 Children(内容在 `Data` 或专属字段)
|
||||
15. ☐ 不要凭空构造 `AttributeViewID`/`block-ref` 的目标 ID(须指向真实存在的块/AV)
|
||||
16. ☐ 不要生成思源禁用的类型(脚注/ToC/YAML/LinkRef/HeadingID 等,见 §11)
|
||||
|
||||
---
|
||||
|
||||
## 13. 禁忌与常见错误
|
||||
|
||||
| ❌ 错误 | ✅ 正确 |
|
||||
|---|---|
|
||||
| 假设所有节点都有 `Data` | `Data` 可省略,marker 节点常无 `Data` |
|
||||
| 内联节点带 `ID` | 内联/标记节点不带 `ID` |
|
||||
| 用 `NodeStrong`/`NodeLink` 等旧式节点 | 用 `NodeTextMark` + `TextMarkType` |
|
||||
| `ListData.Typ` 只认 `1` | `Typ` 缺省=无序,`1`=有序,`3`=任务 |
|
||||
| 把 `BulletChar` 当 base64 | 它是 int ASCII 码点(`42`) |
|
||||
| 任务勾选写 `[x]` 小写 | 大写 `[X]` |
|
||||
| 带样式 TextMark 不配 IAL | 必须配 `NodeKramdownSpanIAL` |
|
||||
| 给 `NodeAttributeView` 加 Children | 它是叶子,用 `AttributeViewID`/`AttributeViewType` |
|
||||
| 改 `ID` 不同步 `Properties.id` | 二者必须一致 |
|
||||
| `inline-math` 带 `TextMarkTextContent` | 它只有 `TextMarkInlineMathContent` |
|
||||
| 凭空造 block-ref / AV 的目标 ID | 目标必须真实存在 |
|
||||
| 把段落直接挂到 `NodeList` 下 | `NodeList` 只能含 `NodeListItem`,必须先包一层 |
|
||||
| 生成脚注/ToC/YAML 等节点 | 思源禁用这些语法,不会出现在 `.sy` 中 |
|
||||
|
||||
---
|
||||
|
||||
## 14. 最小可写文档模板
|
||||
|
||||
```json
|
||||
{
|
||||
"ID": "20260628120000-abc1234",
|
||||
"Spec": "2",
|
||||
"Type": "NodeDocument",
|
||||
"Properties": {
|
||||
"id": "20260628120000-abc1234",
|
||||
"title": "新文档",
|
||||
"type": "doc",
|
||||
"updated": "20260628120000"
|
||||
},
|
||||
"Children": [
|
||||
{
|
||||
"Type": "NodeHeading", "ID": "20260628120001-def5678", "HeadingLevel": 2,
|
||||
"Properties": { "id": "20260628120001-def5678", "updated": "20260628120001" },
|
||||
"Children": [ { "Type": "NodeText", "Data": "标题" } ]
|
||||
},
|
||||
{
|
||||
"Type": "NodeParagraph", "ID": "20260628120002-ghi9012",
|
||||
"Properties": { "id": "20260628120002-ghi9012", "updated": "20260628120002" },
|
||||
"Children": [
|
||||
{ "Type": "NodeText", "Data": "正文含" },
|
||||
{ "Type": "NodeTextMark", "TextMarkType": "strong", "TextMarkTextContent": "加粗" },
|
||||
{ "Type": "NodeText", "Data": "。" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附:核验来源
|
||||
|
||||
- 样本 1:`app/guide/.../20200825162036-4dx365o.sy`(排版元素,覆盖几乎所有块类型)
|
||||
- 样本 2:`app/guide/.../20200905090211-2vixtlf.sy`(内容块类型,含紧凑列表、AttributeView)
|
||||
- 节点类型常量与序列化逻辑:`lute/ast/node.go`、`lute/render/json_renderer.go`、`dataparser/sy.go`
|
||||
- 列表容纳校验:`lute/ast/node.go:988`(`CanContain`)
|
||||
- 思源禁用语法配置:`kernel/util/lute.go:51`(`NewLute`)
|
||||
- 标注 `【推断】` 的字段(如 `file-annotation-ref` 的子字段)建议在生成前用真实样本再核验一次。
|
||||
Reference in New Issue
Block a user