{"record":{"id":"0c35c8af9cbdd6dd","repo":"siyuan-note/siyuan","slug":"heading-s-is-a-leaf-block-and-cannot-have-child","errorCode":null,"errorMessage":"heading [%s] is a leaf block and cannot have children; to place a block below this heading, pass previousID=<heading id> or previousID=<last block below the heading> instead of parentID","messagePattern":"heading \\[(.+?)\\] is a leaf block and cannot have children; to place a block below this heading, pass previousID=<heading id> or previousID=<last block below the heading> instead of parentID","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/treenode/blocktree.go","lineNumber":528,"sourceCode":"\treturn false\n}\n\n// CheckContainerParent 校验 parentID 指向的块是否允许接收子块。\n// 仅在“通过 parentID 定位插入/移动目标”（即不依赖 previousID/nextID）的场景下调用，\n// 因为一旦带 previousID/nextID，事务层走的是兄弟级 InsertAfter/InsertBefore，天然合法。\n// 返回 nil 表示合法；返回 error 时调用方应拒绝本次操作。\nfunc CheckContainerParent(parentID string) error {\n\tbt := GetBlockTree(parentID)\n\tif nil == bt {\n\t\treturn fmt.Errorf(\"parent block not found: %s\", parentID)\n\t}\n\tif IsContainerType(bt.Type) {\n\t\treturn nil\n\t}\n\tif \"h\" == bt.Type {\n\t\t// 标题是叶子块，其“子内容”在数据结构上实为后续兄弟节点（由 HeadingChildren 按层级推算）。\n\t\t// 把块挂成标题的 AST 子节点属于非法嵌套，应改用 previousID 定位。\n\t\treturn fmt.Errorf(\"heading [%s] is a leaf block and cannot have children; to place a block below this heading, pass previousID=<heading id> or previousID=<last block below the heading> instead of parentID\", parentID)\n\t}\n\treturn fmt.Errorf(\"block [%s] type %q is a leaf block and cannot have children; use previousID to place the block as its sibling instead\", parentID, bt.Type)\n}\n\n// CheckListItemNesting 校验 parentID 和 childID 是否形成“列表项直含列表项”的非法嵌套。\n// 嵌套列表的正确结构是 ListItem > List > ListItem，列表项不能直接作为另一个列表项的子块。\n// 仅在 move 场景调用（源和目标类型均已知）。\nfunc CheckListItemNesting(parentID, childID string) error {\n\tparentBt := GetBlockTree(parentID)\n\tchildBt := GetBlockTree(childID)\n\tif nil == parentBt || nil == childBt {\n\t\treturn nil // 查不到就放行，不阻塞未知场景\n\t}\n\tif \"i\" == parentBt.Type && \"i\" == childBt.Type {\n\t\treturn fmt.Errorf(\"a list-item cannot directly contain another list-item; to nest, first create a list (NodeList) under the outer list-item, then add the inner list-items to that list\")\n\t}\n\treturn nil\n}","sourceCodeStart":510,"sourceCodeEnd":546,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/treenode/blocktree.go#L510-L546","documentation":"In SiYuan's data model a heading is a leaf block — content that visually appears 'under' a heading is actually stored as subsequent sibling nodes (computed by HeadingChildren), not AST children. CheckContainerParent rejects using a heading as parentID and instructs the caller to position with previousID instead. The long message documents the exact remedy.","triggerScenarios":"blockInsert/blockAppend with parentID = a heading ID; validateBlockMove targeting a heading as the new parent; plugins that assume headings behave like containers and pass heading IDs as parents.","commonSituations":"Outliner-style plugins inserting notes 'under' headings; scripts migrating outlines that treat headings as folders; move operations whose drop target was computed as a heading container.","solutions":["Pass previousID = the heading ID (new block becomes the sibling right below the heading) instead of parentID","To append after all content under the heading, compute the last block below it (HeadingChildren semantics) and use that as previousID","If you truly need nesting, use a container type (super block, list item, blockquote) rather than a heading"],"exampleFix":"// before\nawait insertBlock({ parentID: headingID, data }) // rejected: heading is a leaf\n// after\nawait insertBlock({ previousID: headingID, data }) // sibling below the heading","handlingStrategy":"validation","validationCode":"const parent = await getBlockInfo(parentID);\nif (parent && parent.type === \"h\") {\n  options.previousID = parentID; delete options.parentID; // heading is a leaf: switch to sibling placement\n}","typeGuard":"function isHeading(info) { return info?.type === \"h\"; }","tryCatchPattern":null,"preventionTips":["Remember: heading children in the UI are stored as siblings — never pass headings as parentID","Use previousID for anything meant to appear under a heading","When building drop targets, expand headings to their HeadingChildren and target the last child instead"],"tags":["document","structure","api"],"backgroundTag":"incompatible-source-type","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}