SY-FORMAT.md
.sy File JSON Structure — AI Read/Write GuideSpec 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.
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.
.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:
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.
{
"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.
| 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.
YYYYMMDDHHMMSS-xxxxxxx = 14-digit timestamp + - + 7 random [a-z0-9] chars. Example: 20210104091228-ttcj9nm.Properties.updated is the same 14-digit timestamp; semantics: "last updated time".ID, you must sync Properties.id. Properties.updated should be refreshed to the current time as well.Leaf blocks: NodeParagraph, NodeHeading, NodeThematicBreak, NodeHTMLBlock, NodeCodeBlock, NodeMathBlock, NodeTable, NodeBlockQueryEmbed, NodeAttributeView, NodeIFrame, NodeVideo, NodeAudio, NodeWidget, NodeCustomBlock, NodeGitConflict
Container blocks: NodeList, NodeListItem, NodeBlockquote, NodeCallout, NodeSuperBlock
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.
{ "Type": "NodeParagraph", "ID": "...", "Properties": { "id": "...", "updated": "..." },
"Children": [ { "Type": "NodeText", "Data": "This is a sample paragraph." } ] }
{ "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.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" } ] }
] }
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.
// 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.
{ "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.
{ "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.
{ "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).
{{ ... }}){ "Type": "NodeBlockQueryEmbed", "ID": "...", "Properties": { "id": "..." },
"Children": [
{ "Type": "NodeOpenBrace" },
{ "Type": "NodeOpenBrace" },
{ "Type": "NodeBlockQueryEmbedScript", "Data": "select * from blocks where id='20210428212840-8rqwn5o'" },
{ "Type": "NodeCloseBrace" },
{ "Type": "NodeCloseBrace" }
] }
{ "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:
NodeCodeBlockCode carries the code content (in Data, raw text with \n escaped); it's an inline child of NodeCodeBlock.CodeBlockInfo is the base64-encoded language ("Z28=" = go). The parent's seven fields (IsFencedCodeBlock/CodeBlockFenceChar/CodeBlockFenceLen/CodeBlockOpenFence/CodeBlockInfo/CodeBlockCloseFence) all carry omitempty and may be omitted as needed — newer .sy files often write only "IsFencedCodeBlock": true.SetIndentCodeBlock(false)); all code blocks are fenced.{ "Type": "NodeMathBlock", "ID": "...", "Properties": { "id": "..." },
"Children": [
{ "Type": "NodeMathBlockOpenMarker" },
{ "Type": "NodeMathBlockContent", "Data": "a^2 + b^2 = c^2" },
{ "Type": "NodeMathBlockCloseMarker" }
] }
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.
{ "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" } ] }
] }
] }
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.Properties.colgroup (|-separated).{ "Type": "NodeAttributeView", "ID": "...",
"Properties": { "custom-sy-av-view": "20251230141609-lcme2fh", "id": "...", "updated": "..." },
"AttributeViewID": "20251230141609-2kvghrg",
"AttributeViewType": "table" }
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.
{ "Type": "NodeThematicBreak", "ID": "...", "Properties": { "id": "..." } }
NodeText (plain text){ "Type": "NodeText", "Data": "plain text" }
Data may be omitted (common as a placeholder zero-width space: { "Type": "NodeText" }).
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 node NodeBlockQueryEmbed, unrelated to this).TextMarkType may stack multiple marks separated by spaces, e.g. "strong em".TextMarkTextContent is not present on every type (inline-math lacks it).~~x~~, not single-tilde ~x~ (SetGFMStrikethrough1(false)).NodeTextMark subtype: it maps to the separate NodeBackslash node and never appears as a TextMarkType value.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.
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" }
] }
Data = "span".NodeBang/NodeOpenBracket/NodeCloseBracket/NodeOpenParen/NodeCloseParen markers may omit Data.NodeLinkText and NodeLinkDest carry Data.| 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.
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.
| 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.
.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.
.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.
When generating a .sy that SiYuan can load cleanly, verify item by item:
Type = "NodeDocument", Spec = "2"; root ID = filename (without .sy) and equals Properties.idProperties contains id/title/type:"doc"/updatedID; Properties.id = ID; Properties.updated is a valid 14-digit timestampIDListData.Typ (omitted = unordered / 1 = ordered / 3 = task)NodeList's direct children are only NodeListItem; nested lists wrap another NodeListBulletChar/Delimiter are byte (appear as int codepoints in JSON); Marker is base64Data (checked items use TaskListItemChecked:true, unchecked only Type)NodeCodeBlockCode/NodeMathBlockContent are inline children, usually only Type+DataNodeTextMark for inline formatting over legacy NodeStrong/NodeEmphasis/NodeLinkTextMark must be followed by a paired NodeKramdownSpanIALChildren (content in Data or type-specific fields)AttributeViewID/block-ref target IDs (they must point to real blocks/AVs)| ❌ 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 |
{
"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": "." }
]
}
]
}
app/guide/.../20200825162036-4dx365o.sy (formatting elements — covers nearly all block types)app/guide/.../20200905090211-2vixtlf.sy (block types — incl. compact lists, AttributeView)lute/ast/node.go, lute/render/json_renderer.go, dataparser/sy.golute/ast/node.go:988 (CanContain)kernel/util/lute.go:51 (NewLute)【inferred】 (e.g. the sub-fields of file-annotation-ref) should be re-verified against a real sample before generation.