Signed-off-by: Daniel <845765@qq.com>
29 KiB
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. Companion document:WORKSPACE.mdcovers the overall on-disk layout of the workspace (how notebooks, parent/child documents, and assets are organized); this document focuses on the internal JSON structure of a.syfile.
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; for the workspace's on-disk layout see
WORKSPACE.md) - 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
{
"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. For the full file-system layout seeWORKSPACE.md.
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.updatedis the same 14-digit timestamp; semantics: "last updated time".- When you change any block's
ID, you must syncProperties.id.Properties.updatedshould 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
Inline / marker nodes (no ID)
NodeText, NodeTextMark, NodeImage, NodeKramdownSpanIAL, NodeHeadingC8hMarker, NodeBlockquoteMarker, NodeTaskListItemMarker, NodeBang, NodeOpenBracket, NodeCloseBracket, NodeOpenParen, NodeCloseParen, NodeLinkText, NodeLinkDest, NodeCodeBlockCode, NodeCodeBlockFenceOpenMarker, NodeCodeBlockFenceInfoMarker, NodeCodeBlockFenceCloseMarker, NodeMathBlockContent, 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
{ "Type": "NodeParagraph", "ID": "...", "Properties": { "id": "...", "updated": "..." },
"Children": [ { "Type": "NodeText", "Data": "This is a sample paragraph." } ] }
5.2 Heading
{ "Type": "NodeHeading", "ID": "...", "HeadingLevel": 2,
"Properties": { "id": "...", "updated": "..." },
"Children": [ { "Type": "NodeText", "Data": "Heading" } ] }
HeadingLevelranges1–6.NodeHeadingC8hMarker(Datasuch 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
NodeListcan only beNodeListItem(enforced by Lute'sCanContain,ast/node.go:993return NodeListItem == nodeType). Paragraphs, code blocks, sub-lists, or any other block cannot be attached directly underNodeList— they must be wrapped in aNodeListItemfirst.
✅ 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):
{ "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):
{ "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:
{ "Type": "NodeListItem", "ID": "...",
"ListData": { "Typ": 3, "Tight": true, "BulletChar": 45, "Padding": 2, "Checked": true, "Marker": "LQ==", "Num": -1 },
"Properties": { "id": "..." },
"Children": [
{ "Type": "NodeTaskListItemMarker", "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 (code) | JSON form | Meaning |
|---|---|---|---|
Typ |
int | number | List type discriminator: omitted = unordered, 1 = ordered, 3 = task |
Tight |
bool | boolean | Tight (no blank lines); optional |
BulletChar |
byte | number | Bullet ASCII codepoint for unordered/task lists (42 = *, 45 = -) |
Delimiter |
byte | number | Ordered-list delimiter ASCII codepoint (46 = .) |
Start |
int | number | Ordered-list start number |
Num |
int | number | This item's number; usually omitted or -1 for unordered/task lists |
Padding |
int | number | Indent padding; optional |
Checked |
bool | boolean | Whether the task item is checked (aggregated at the list level) |
Marker |
[]byte | base64 string | The marker text, base64-encoded; may include a delimiter ("MS4=" = 1.) or not ("MQ==" = 1) |
Key distinction:
BulletChar/Delimiterarebytein code and appear as int codepoints in JSON;Markeris[]bytein code and appears as a base64 string in JSON.Marker/BulletChar/Delimiterall carryomitemptyand may be omitted.
5.5 Task marker
// Checked
{ "Type": "NodeTaskListItemMarker", "TaskListItemChecked": true }
// Unchecked
{ "Type": "NodeTaskListItemMarker" }
In real
.syfiles,NodeTaskListItemMarkerusually has noDatafield (SiYuan rebuilds the marker from the DOMdata-taskattribute and doesn't keep the[X]/[ ]source): checked items carry onlyType+TaskListItemChecked, unchecked onlyType. If you do writeDataby hand, the checked form should be uppercase[X](not[x]) — but that's only used for normalized Markdown export.
5.6 Blockquote
{ "Type": "NodeBlockquote", "ID": "...", "Properties": { "id": "..." },
"Children": [
{ "Type": "NodeBlockquoteMarker", "Data": "> " },
{ "Type": "NodeParagraph", "ID": "...", "Properties": { "id": "..." },
"Children": [ { "Type": "NodeText", "Data": "Quoted content" } ] }
] }
NodeBlockquoteMarker.Datamay be">"or"> "— both are legal.
5.7 Callout (GFM Alert)
{ "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 |
🚨 |
CalloutIconis a literal emoji character, not base64, not a codepoint.
5.8 Super block (nestable; three-part structure)
{ "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.Datacan 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 {{ ... }})
{ "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)
{ "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", "Data": "package main\n...\n" },
{ "Type": "NodeCodeBlockFenceCloseMarker", "Data": "```", "CodeBlockFenceLen": 3 }
] }
Notes:
NodeCodeBlockCodecarries the code content (inData, raw text with\nescaped); it's an inline child ofNodeCodeBlock.- The surrounding fence markers (Open/Info/Close) are likewise inline children.
CodeBlockInfois the base64-encoded language ("Z28="=go). The parent's seven fields (IsFencedCodeBlock/CodeBlockFenceChar/CodeBlockFenceLen/CodeBlockOpenFence/CodeBlockInfo/CodeBlockCloseFence) all carryomitemptyand may be omitted as needed — newer.syfiles often write only"IsFencedCodeBlock": true.- SiYuan does not support indented code blocks (
SetIndentCodeBlock(false)); all code blocks are fenced.
5.11 Math block (three-part structure)
{ "Type": "NodeMathBlock", "ID": "...", "Properties": { "id": "..." },
"Children": [
{ "Type": "NodeMathBlockOpenMarker" },
{ "Type": "NodeMathBlockContent", "Data": "a^2 + b^2 = c^2" },
{ "Type": "NodeMathBlockCloseMarker" }
] }
5.12 HTML / IFrame / Video / Audio blocks (leaf; content in the top-level Data)
{ "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-levelData.
5.13 Table
{ "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)
{ "Type": "NodeAttributeView", "ID": "...",
"Properties": { "custom-sy-av-view": "20251230141609-lcme2fh", "id": "...", "updated": "..." },
"AttributeViewID": "20251230141609-2kvghrg",
"AttributeViewType": "table" }
- Has no
Children. AttributeViewIDpoints to the AV table data (stored in a separate.json— don't fabricate this ID).AttributeViewType:table/kanban/gallery, etc.custom-sy-av-viewrecords 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
{ "Type": "NodeThematicBreak", "ID": "...", "Properties": { "id": "..." } }
6. Inline nodes in detail
6.1 NodeText (plain text)
{ "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, TextMarkTextContent |
Samples:
{ "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 anchor text,"d"= dynamic anchor text (the anchor text follows the target block's content; note that "embed block" is a separate nodeNodeBlockQueryEmbed, unrelated to this).TextMarkTypemay stack multiple marks separated by spaces, e.g."strong em".TextMarkTextContentis not present on every type (inline-mathlacks it).- Strikethrough supports only double-tilde
~~x~~, not single-tilde~x~(SetGFMStrikethrough1(false)). - Backslash escape is not a
NodeTextMarksubtype: it maps to the separateNodeBackslashnode and never appears as aTextMarkTypevalue.
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:
{ "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)
{ "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/NodeCloseParenmarkers may omitData.- Only
NodeLinkTextandNodeLinkDestcarryData.
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/Infoare base64; content fields likeData,TextMarkTextContent,TextMarkInlineMathContentare raw;BulletChar/Delimiterare 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 uppercaseID— 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 |
Note:
NewLute()also setsSetAutoSpace(false),SetCodeSyntaxHighlight(false), andSetExportNormalizeTaskListMarker(false)— these are non-syntax switches that only affect rendering/export and never remove any node type, so they're omitted from the table above.
12. AI write checklist
When generating a .sy that SiYuan can load cleanly, verify item by item:
- ☐ Root
Type="NodeDocument",Spec="2"; rootID= filename (without.sy) and equalsProperties.id - ☐ Root
Propertiescontainsid/title/type:"doc"/updated - ☐ Every block has a 22-char
ID;Properties.id=ID;Properties.updatedis a valid 14-digit timestamp - ☐ Inline/marker nodes do not carry
ID - ☐ Lists are distinguished via
ListData.Typ(omitted = unordered /1= ordered /3= task) - ☐
NodeList's direct children are onlyNodeListItem; nested lists wrap anotherNodeList - ☐
BulletChar/Delimiterare byte (appear as int codepoints in JSON);Markeris base64 - ☐ Task markers usually carry no
Data(checked items useTaskListItemChecked:true, unchecked onlyType) - ☐ Code block four parts, math block three parts, embed five parts, super block three parts — structure intact
- ☐
NodeCodeBlockCode/NodeMathBlockContentare inline children, usually onlyType+Data - ☐ base64 fields are encoded; content fields stay raw
- ☐ Prefer
NodeTextMarkfor inline formatting over legacyNodeStrong/NodeEmphasis/NodeLink - ☐ A styled
TextMarkmust be followed by a pairedNodeKramdownSpanIAL - ☐ HTML/IFrame/Video/Audio/AttributeView are leaves with no
Children(content inDataor type-specific fields) - ☐ Do not fabricate
AttributeViewID/block-reftarget IDs (they must point to real blocks/AVs) - ☐ 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 byte, appearing as an int codepoint in JSON (42 = *) |
Forcing "Data":"[X]" into a task marker |
Real .sy usually has no Data; checked items only carry TaskListItemChecked:true |
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
{
"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 offile-annotation-ref) should be re-verified against a real sample before generation.