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

  1. Create a child list (NodeList) under the outer list item first, then move the inner list item into that list.
  2. Alternatively move the item so it becomes a sibling via previousID=<another list item id> instead of nesting.
  3. 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

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


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)