siyuan-note/siyuan · error
a list-item cannot directly contain another list-item; to…
Error message
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
What it means
CheckListItemNesting rejects a move/insert that would make a list item ('i') the direct child of another list item. The correct nesting in the AST is ListItem > List > ListItem: an inner list must sit between the two list items. It is only checked in move scenarios where both source and target types are known.
Solutions
- Create a child list (NodeList) under the outer list item first, then move the inner list item into that list.
- Alternatively move the item so it becomes a sibling via previousID=<another list item id> instead of nesting.
- Before moving, check both blocks' types with GetBlockTree and refuse/adjust when both are type 'i'.
Example fix
// before
moveBlock({id: innerItemID, parentID: outerItemID})
// after
createBlock({dataType: "markdown", data: "*", parentID: outerItemID}) // child list
moveBlock({id: innerItemID, parentID: newChildListID}) Defensive patterns
Strategy: validation
Validate before calling
const parent = (await fetchPost('/api/block/getBlockInfo', {id: parentID})).data;
const child = (await fetchPost('/api/block/getBlockInfo', {id: childID})).data;
if (parent.subType === 'o' || parent.type === 'NodeListItem') { /* ensure a child list exists or use previousID */ } Prevention
- Never move a list item directly under another list item; always insert an intermediate list.
- Check both block types before a programmatic move.
- Prefer sibling placement (previousID) for list reordering.
When it happens
Trigger: Calling blockMove (or validateBlockMove) with parentID pointing to a list item and the moved block itself being a list item, without an intermediate list node.
Common situations: Dragging or scripting a list item under another list item expecting plain parent-child nesting; tools that flatten a list and re-parent items; programmatic outline restructuring that skips creating the intermediate NodeList.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- block [ ] type is a leaf block and cannot have children…
- document block [ ] cannot be moved with block move; use…
- document block [ ] cannot be used as a previous sibling…
- previous block not found
- rich text source belongs to a table cell
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/259402b1f38ab57e.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/treenode/blocktree.go:543
if "h" == bt.Type {
// 标题是叶子块,其“子内容”在数据结构上实为后续兄弟节点(由 HeadingChildren 按层级推算)。
// 把块挂成标题的 AST 子节点属于非法嵌套,应改用 previousID 定位。
return 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)
}
return 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)
}
// CheckListItemNesting 校验 parentID 和 childID 是否形成“列表项直含列表项”的非法嵌套。
// 嵌套列表的正确结构是 ListItem > List > ListItem,列表项不能直接作为另一个列表项的子块。
// 仅在 move 场景调用(源和目标类型均已知)。
func CheckListItemNesting(parentID, childID string) error {
parentBt := GetBlockTree(parentID)
childBt := GetBlockTree(childID)
if nil == parentBt || nil == childBt {
return nil // 查不到就放行,不阻塞未知场景
}
if "i" == parentBt.Type && "i" == childBt.Type {
return 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")
}
return nil
}
func SetBlockTreePath(tree *parse.Tree) {
RemoveBlockTreesByRootID(tree.Box, tree.ID)
IndexBlockTree(tree)
}
func RemoveBlockTreesByRootID(boxID, rootID string) {
sqlStmt := "DELETE FROM blocktrees WHERE root_id = ?"
_, err := execForBox(boxID, sqlStmt, rootID)
if err != nil {
logging.LogErrorf("sql exec [%s] failed: %s", sqlStmt, err)
return
}
}
View on GitHub (pinned to 9f775e8a12)