siyuan/kernel/model/block.go

923 lines
22 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package model
import (
"bytes"
"errors"
"fmt"
"github.com/88250/lute/render"
"strconv"
"strings"
"time"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/open-spaced-repetition/go-fsrs/v3"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
// Block 描述了内容块。
type Block struct {
Box string `json:"box"`
Path string `json:"path"`
HPath string `json:"hPath"`
ID string `json:"id"`
RootID string `json:"rootID"`
ParentID string `json:"parentID"`
Name string `json:"name"`
Alias string `json:"alias"`
Memo string `json:"memo"`
Tag string `json:"tag"`
Content string `json:"content"`
FContent string `json:"fcontent"`
Markdown string `json:"markdown"`
Folded bool `json:"folded"`
Type string `json:"type"`
SubType string `json:"subType"`
RefText string `json:"refText"`
Defs []*Block `json:"-"` // 当前块引用了这些块,避免序列化 JSON 时产生循环引用
Refs []*Block `json:"refs"` // 当前块被这些块引用
DefID string `json:"defID"`
DefPath string `json:"defPath"`
IAL map[string]string `json:"ial"`
Children []*Block `json:"children"`
Depth int `json:"depth"`
Count int `json:"count"`
Sort int `json:"sort"`
Created string `json:"created"`
Updated string `json:"updated"`
RiffCardID string `json:"riffCardID"`
RiffCard *RiffCard `json:"riffCard"`
}
type RiffCard struct {
Due time.Time `json:"due"`
Reps uint64 `json:"reps"`
Lapses uint64 `json:"lapses"`
State fsrs.State `json:"state"`
LastReview time.Time `json:"lastReview"`
}
func getRiffCard(card *fsrs.Card) *RiffCard {
due := card.Due
if due.IsZero() {
due = time.Now()
}
return &RiffCard{
Due: due,
Reps: card.Reps,
Lapses: card.Lapses,
State: card.State,
LastReview: card.LastReview,
}
}
func (block *Block) IsContainerBlock() bool {
switch block.Type {
case "NodeDocument", "NodeBlockquote", "NodeList", "NodeListItem", "NodeSuperBlock":
return true
}
return false
}
func (block *Block) IsDoc() bool {
return "NodeDocument" == block.Type
}
type Path struct {
ID string `json:"id"` // 块 ID
Box string `json:"box"` // 块 Box
Name string `json:"name"` // 当前路径
HPath string `json:"hPath"` // 人类可读路径
Type string `json:"type"` // "path"
NodeType string `json:"nodeType"` // 节点类型
SubType string `json:"subType"` // 节点子类型
Blocks []*Block `json:"blocks,omitempty"` // 子块节点
Children []*Path `json:"children,omitempty"` // 子路径节点
Depth int `json:"depth"` // 层级深度
Count int `json:"count"` // 子块计数
Updated string `json:"updated"` // 更新时间
Created string `json:"created"` // 创建时间
}
type BlockTreeInfo struct {
ID string `json:"id"`
Type string `json:"type"`
ParentID string `json:"parentID"`
ParentType string `json:"parentType"`
PreviousID string `json:"previousID"`
PreviousType string `json:"previousType"`
NextID string `json:"nextID"`
NextType string `json:"nextType"`
}
func GetBlockTreeInfos(ids []string) (ret map[string]*BlockTreeInfo) {
ret = map[string]*BlockTreeInfo{}
trees := filesys.LoadTrees(ids)
for id, tree := range trees {
node := treenode.GetNodeInTree(tree, id)
if nil == node {
ret[id] = &BlockTreeInfo{ID: id}
continue
}
bti := &BlockTreeInfo{ID: id, Type: node.Type.String()}
ret[id] = bti
parent := treenode.ParentBlock(node)
if nil != parent {
bti.ParentID = parent.ID
bti.ParentType = parent.Type.String()
}
previous := treenode.PreviousBlock(node)
if nil != previous {
bti.PreviousID = previous.ID
bti.PreviousType = previous.Type.String()
}
next := treenode.NextBlock(node)
if nil != next {
bti.NextID = next.ID
bti.NextType = next.Type.String()
}
}
return
}
func GetBlockSiblingID(id string) (parent, previous, next string) {
tree, err := LoadTreeByBlockID(id)
if err != nil {
return
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
return
}
if !node.IsBlock() {
return
}
parentListCount := 0
var parentListItem, current *ast.Node
for p := node.Parent; nil != p; p = p.Parent {
if ast.NodeListItem == p.Type {
parentListCount++
if 1 < parentListCount {
parentListItem = p
break
}
current = p.Parent
}
}
if nil != parentListItem {
parent = parentListItem.ID
if parentListItem.Previous != nil {
previous = parentListItem.Previous.ID
if flb := treenode.FirstChildBlock(parentListItem.Previous); nil != flb {
previous = flb.ID
}
}
if parentListItem.Next != nil {
next = parentListItem.Next.ID
if flb := treenode.FirstChildBlock(parentListItem.Next); nil != flb {
next = flb.ID
}
}
return
}
if nil == current {
current = node
}
if nil != current.Parent && current.Parent.IsBlock() {
parent = current.Parent.ID
if flb := treenode.FirstChildBlock(current.Parent); nil != flb {
parent = flb.ID
}
if ast.NodeDocument == current.Parent.Type {
parent = current.Parent.ID
if nil != current.Previous && current.Previous.IsBlock() {
previous = current.Previous.ID
if flb := treenode.FirstChildBlock(current.Previous); nil != flb {
previous = flb.ID
}
}
if nil != current.Next && current.Next.IsBlock() {
next = current.Next.ID
if flb := treenode.FirstChildBlock(current.Next); nil != flb {
next = flb.ID
}
}
} else {
if nil != current.Parent.Previous && current.Parent.Previous.IsBlock() {
previous = current.Parent.Previous.ID
if flb := treenode.FirstChildBlock(current.Parent.Previous); nil != flb {
previous = flb.ID
}
}
if nil != current.Parent.Next && current.Parent.Next.IsBlock() {
next = current.Parent.Next.ID
if flb := treenode.FirstChildBlock(current.Parent.Next); nil != flb {
next = flb.ID
}
}
}
}
return
}
func IsBlockFolded(id string) (isFolded, isRoot bool) {
tree, _ := LoadTreeByBlockID(id)
if nil == tree {
return
}
if tree.Root.ID == id {
isRoot = true
}
for i := 0; i < 32; i++ {
b, _ := getBlock(id, nil)
if nil == b {
return
}
if "1" == b.IAL["fold"] {
isFolded = true
return
}
id = b.ParentID
}
return
}
func RecentUpdatedBlocks() (ret []*Block) {
ret = []*Block{}
sqlStmt := "SELECT * FROM blocks WHERE type = 'p' AND length > 1"
if util.ContainerIOS == util.Container || util.ContainerAndroid == util.Container || util.ContainerHarmony == util.Container {
sqlStmt = "SELECT * FROM blocks WHERE type = 'd'"
}
if ignoreLines := getSearchIgnoreLines(); 0 < len(ignoreLines) {
// Support ignore search results https://github.com/siyuan-note/siyuan/issues/10089
buf := bytes.Buffer{}
for _, line := range ignoreLines {
buf.WriteString(" AND ")
buf.WriteString(line)
}
sqlStmt += buf.String()
}
sqlStmt += " ORDER BY updated DESC"
sqlBlocks := sql.SelectBlocksRawStmt(sqlStmt, 1, 16)
if 1 > len(sqlBlocks) {
return
}
ret = fromSQLBlocks(&sqlBlocks, "", 0)
return
}
func TransferBlockRef(fromID, toID string, refIDs []string) (err error) {
toTree, _ := LoadTreeByBlockID(toID)
if nil == toTree {
err = ErrBlockNotFound
return
}
toNode := treenode.GetNodeInTree(toTree, toID)
if nil == toNode {
err = ErrBlockNotFound
return
}
toRefText := getNodeRefText(toNode)
util.PushMsg(Conf.Language(116), 7000)
if 1 > len(refIDs) { // 如果不指定 refIDs则转移所有引用了 fromID 的块
refIDs, _ = sql.QueryRefIDsByDefID(fromID, false)
}
trees := filesys.LoadTrees(refIDs)
for refID, tree := range trees {
if nil == tree {
continue
}
node := treenode.GetNodeInTree(tree, refID)
textMarks := node.ChildrenByType(ast.NodeTextMark)
for _, textMark := range textMarks {
if textMark.IsTextMarkType("block-ref") && textMark.TextMarkBlockRefID == fromID {
textMark.TextMarkBlockRefID = toID
if "d" == textMark.TextMarkBlockRefSubtype {
textMark.TextMarkTextContent = toRefText
}
}
}
if err = indexWriteTreeUpsertQueue(tree); err != nil {
return
}
}
sql.FlushQueue()
return
}
func SwapBlockRef(refID, defID string, includeChildren bool) (err error) {
refTree, err := LoadTreeByBlockID(refID)
if err != nil {
return
}
refNode := treenode.GetNodeInTree(refTree, refID)
if nil == refNode {
return
}
if ast.NodeListItem == refNode.Parent.Type {
refNode = refNode.Parent
}
defTree, err := LoadTreeByBlockID(defID)
if err != nil {
return
}
sameTree := defTree.ID == refTree.ID
var defNode *ast.Node
if !sameTree {
defNode = treenode.GetNodeInTree(defTree, defID)
} else {
defNode = treenode.GetNodeInTree(refTree, defID)
}
if nil == defNode {
return
}
var defNodeChildren []*ast.Node
if ast.NodeListItem == defNode.Parent.Type {
defNode = defNode.Parent
} else if ast.NodeHeading == defNode.Type && includeChildren {
defNodeChildren = treenode.HeadingChildren(defNode)
}
if ast.NodeListItem == defNode.Type {
for c := defNode.FirstChild; nil != c; c = c.Next {
if ast.NodeList == c.Type {
defNodeChildren = append(defNodeChildren, c)
}
}
}
refreshUpdated(defNode)
refreshUpdated(refNode)
refPivot := treenode.NewParagraph("")
refNode.InsertBefore(refPivot)
if ast.NodeListItem == defNode.Type {
if ast.NodeListItem == refNode.Type {
if !includeChildren {
for _, c := range defNodeChildren {
refNode.AppendChild(c)
}
}
defNode.InsertAfter(refNode)
refPivot.InsertAfter(defNode)
} else {
newID := ast.NewNodeID()
li := &ast.Node{ID: newID, Type: ast.NodeListItem, ListData: &ast.ListData{Typ: defNode.Parent.ListData.Typ}}
li.SetIALAttr("id", newID)
li.SetIALAttr("updated", newID[:14])
li.AppendChild(refNode)
defNode.InsertAfter(li)
if !includeChildren {
for _, c := range defNodeChildren {
li.AppendChild(c)
}
}
newID = ast.NewNodeID()
list := &ast.Node{ID: newID, Type: ast.NodeList, ListData: &ast.ListData{Typ: defNode.Parent.ListData.Typ}}
list.SetIALAttr("id", newID)
list.SetIALAttr("updated", newID[:14])
list.AppendChild(defNode)
refPivot.InsertAfter(list)
}
} else {
if ast.NodeListItem == refNode.Type {
newID := ast.NewNodeID()
list := &ast.Node{ID: newID, Type: ast.NodeList, ListData: &ast.ListData{Typ: refNode.Parent.ListData.Typ}}
list.SetIALAttr("id", newID)
list.SetIALAttr("updated", newID[:14])
list.AppendChild(refNode)
defNode.InsertAfter(list)
newID = ast.NewNodeID()
li := &ast.Node{ID: newID, Type: ast.NodeListItem, ListData: &ast.ListData{Typ: refNode.Parent.ListData.Typ}}
li.SetIALAttr("id", newID)
li.SetIALAttr("updated", newID[:14])
li.AppendChild(defNode)
for i := len(defNodeChildren) - 1; -1 < i; i-- {
defNode.InsertAfter(defNodeChildren[i])
}
refPivot.InsertAfter(li)
} else {
defNode.InsertAfter(refNode)
refPivot.InsertAfter(defNode)
for i := len(defNodeChildren) - 1; -1 < i; i-- {
defNode.InsertAfter(defNodeChildren[i])
}
}
}
refPivot.Unlink()
if err = indexWriteTreeUpsertQueue(refTree); err != nil {
return
}
if !sameTree {
if err = indexWriteTreeUpsertQueue(defTree); err != nil {
return
}
}
FlushTxQueue()
util.ReloadUI()
return
}
func GetHeadingDeleteTransaction(id string) (transaction *Transaction, err error) {
tree, err := LoadTreeByBlockID(id)
if err != nil {
return
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
err = errors.New(fmt.Sprintf(Conf.Language(15), id))
return
}
if ast.NodeHeading != node.Type {
return
}
var nodes []*ast.Node
nodes = append(nodes, node)
nodes = append(nodes, treenode.HeadingChildren(node)...)
transaction = &Transaction{}
luteEngine := util.NewLute()
for _, n := range nodes {
op := &Operation{}
op.ID = n.ID
op.Action = "delete"
transaction.DoOperations = append(transaction.DoOperations, op)
op = &Operation{}
op.ID = n.ID
if nil != n.Parent {
op.ParentID = n.Parent.ID
}
if nil != n.Previous {
op.PreviousID = n.Previous.ID
}
op.Action = "insert"
op.Data = luteEngine.RenderNodeBlockDOM(n)
transaction.UndoOperations = append(transaction.UndoOperations, op)
}
return
}
func GetHeadingChildrenIDs(id string) (ret []string) {
tree, err := LoadTreeByBlockID(id)
if err != nil {
return
}
heading := treenode.GetNodeInTree(tree, id)
if nil == heading || ast.NodeHeading != heading.Type {
return
}
children := treenode.HeadingChildren(heading)
nodes := append([]*ast.Node{}, children...)
for _, n := range nodes {
ret = append(ret, n.ID)
}
return
}
func GetHeadingChildrenDOM(id string) (ret string) {
tree, err := LoadTreeByBlockID(id)
if err != nil {
return
}
heading := treenode.GetNodeInTree(tree, id)
if nil == heading || ast.NodeHeading != heading.Type {
return
}
nodes := append([]*ast.Node{}, heading)
children := treenode.HeadingChildren(heading)
nodes = append(nodes, children...)
// 取消折叠 https://github.com/siyuan-note/siyuan/issues/13232#issuecomment-2535955152
for _, child := range children {
ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
n.RemoveIALAttr("heading-fold")
n.RemoveIALAttr("fold")
return ast.WalkContinue
})
}
heading.RemoveIALAttr("fold")
heading.RemoveIALAttr("heading-fold")
luteEngine := util.NewLute()
ret = renderBlockDOMByNodes(nodes, luteEngine)
return
}
func GetHeadingLevelTransaction(id string, level int) (transaction *Transaction, err error) {
tree, err := LoadTreeByBlockID(id)
if err != nil {
return
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
err = errors.New(fmt.Sprintf(Conf.Language(15), id))
return
}
if ast.NodeHeading != node.Type {
return
}
hLevel := node.HeadingLevel
if hLevel == level {
return
}
diff := level - hLevel
var children, childrenHeadings []*ast.Node
children = append(children, node)
children = append(children, treenode.HeadingChildren(node)...)
for _, c := range children {
ccH := c.ChildrenByType(ast.NodeHeading)
childrenHeadings = append(childrenHeadings, ccH...)
}
transaction = &Transaction{}
luteEngine := util.NewLute()
for _, c := range childrenHeadings {
op := &Operation{}
op.ID = c.ID
op.Action = "update"
op.Data = luteEngine.RenderNodeBlockDOM(c)
transaction.UndoOperations = append(transaction.UndoOperations, op)
c.HeadingLevel += diff
if 6 < c.HeadingLevel {
c.HeadingLevel = 6
} else if 1 > c.HeadingLevel {
c.HeadingLevel = 1
}
op = &Operation{}
op.ID = c.ID
op.Action = "update"
op.Data = luteEngine.RenderNodeBlockDOM(c)
transaction.DoOperations = append(transaction.DoOperations, op)
}
return
}
func GetBlockDOM(id string) (ret string) {
if "" == id {
return
}
tree, err := LoadTreeByBlockID(id)
if err != nil {
return
}
node := treenode.GetNodeInTree(tree, id)
luteEngine := NewLute()
ret = luteEngine.RenderNodeBlockDOM(node)
return
}
func GetBlockKramdown(id, mode string) (ret string) {
if "" == id {
return
}
tree, err := LoadTreeByBlockID(id)
if err != nil {
return
}
addBlockIALNodes(tree, false)
node := treenode.GetNodeInTree(tree, id)
root := &ast.Node{Type: ast.NodeDocument}
root.AppendChild(node.Next) // IAL
root.PrependChild(node)
luteEngine := NewLute()
if "md" == mode {
ret = treenode.ExportNodeStdMd(root, luteEngine)
} else {
tree.Root = root
formatRenderer := render.NewFormatRenderer(tree, luteEngine.RenderOptions)
ret = string(formatRenderer.Render())
}
return
}
type ChildBlock struct {
ID string `json:"id"`
Type string `json:"type"`
SubType string `json:"subType,omitempty"`
Content string `json:"content,omitempty"`
Markdown string `json:"markdown,omitempty"`
}
func GetChildBlocks(id string) (ret []*ChildBlock) {
ret = []*ChildBlock{}
if "" == id {
return
}
tree, err := LoadTreeByBlockID(id)
if err != nil {
return
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
return
}
if ast.NodeHeading == node.Type {
children := treenode.HeadingChildren(node)
for _, c := range children {
block := sql.BuildBlockFromNode(c, tree)
ret = append(ret, &ChildBlock{
ID: c.ID,
Type: treenode.TypeAbbr(c.Type.String()),
SubType: treenode.SubTypeAbbr(c),
Content: block.Content,
Markdown: block.Markdown,
})
}
return
}
if !node.IsContainerBlock() {
return
}
for c := node.FirstChild; nil != c; c = c.Next {
if !c.IsBlock() {
continue
}
block := sql.BuildBlockFromNode(c, tree)
ret = append(ret, &ChildBlock{
ID: c.ID,
Type: treenode.TypeAbbr(c.Type.String()),
SubType: treenode.SubTypeAbbr(c),
Content: block.Content,
Markdown: block.Markdown,
})
}
return
}
func GetTailChildBlocks(id string, n int) (ret []*ChildBlock) {
ret = []*ChildBlock{}
if "" == id {
return
}
tree, err := LoadTreeByBlockID(id)
if err != nil {
return
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
return
}
if ast.NodeHeading == node.Type {
children := treenode.HeadingChildren(node)
for i := len(children) - 1; 0 <= i; i-- {
c := children[i]
block := sql.BuildBlockFromNode(c, tree)
ret = append(ret, &ChildBlock{
ID: c.ID,
Type: treenode.TypeAbbr(c.Type.String()),
SubType: treenode.SubTypeAbbr(c),
Content: block.Content,
Markdown: block.Markdown,
})
if n == len(ret) {
return
}
}
return
}
if !node.IsContainerBlock() {
return
}
for c := node.LastChild; nil != c; c = c.Previous {
if !c.IsBlock() {
continue
}
block := sql.BuildBlockFromNode(c, tree)
ret = append(ret, &ChildBlock{
ID: c.ID,
Type: treenode.TypeAbbr(c.Type.String()),
SubType: treenode.SubTypeAbbr(c),
Content: block.Content,
Markdown: block.Markdown,
})
if n == len(ret) {
return
}
}
return
}
func GetBlock(id string, tree *parse.Tree) (ret *Block, err error) {
ret, err = getBlock(id, tree)
return
}
func getBlock(id string, tree *parse.Tree) (ret *Block, err error) {
if "" == id {
return
}
if nil == tree {
tree, err = LoadTreeByBlockID(id)
if err != nil {
time.Sleep(1 * time.Second)
tree, err = LoadTreeByBlockID(id)
if err != nil {
return
}
}
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
err = ErrBlockNotFound
return
}
sqlBlock := sql.BuildBlockFromNode(node, tree)
if nil == sqlBlock {
return
}
ret = fromSQLBlock(sqlBlock, "", 0)
return
}
func getEmbeddedBlock(trees map[string]*parse.Tree, sqlBlock *sql.Block, headingMode int, breadcrumb bool) (block *Block, blockPaths []*BlockPath) {
tree, _ := trees[sqlBlock.RootID]
if nil == tree {
tree, _ = LoadTreeByBlockID(sqlBlock.RootID)
}
if nil == tree {
return
}
def := treenode.GetNodeInTree(tree, sqlBlock.ID)
if nil == def {
return
}
var unlinks, nodes []*ast.Node
ast.Walk(def, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeHeading == n.Type {
if "1" == n.IALAttr("fold") {
children := treenode.HeadingChildren(n)
for _, c := range children {
unlinks = append(unlinks, c)
}
}
}
return ast.WalkContinue
})
for _, n := range unlinks {
n.Unlink()
}
nodes = append(nodes, def)
if 0 == headingMode && ast.NodeHeading == def.Type && "1" != def.IALAttr("fold") {
children := treenode.HeadingChildren(def)
for _, c := range children {
if "1" == c.IALAttr("heading-fold") {
// 嵌入块包含折叠标题时不应该显示其下方块 https://github.com/siyuan-note/siyuan/issues/4765
continue
}
nodes = append(nodes, c)
}
}
b := treenode.GetBlockTree(def.ID)
if nil == b {
return
}
// 嵌入块查询结果中显示块引用计数 https://github.com/siyuan-note/siyuan/issues/7191
var defIDs []string
for _, n := range nodes {
ast.Walk(n, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if n.IsBlock() {
defIDs = append(defIDs, n.ID)
}
return ast.WalkContinue
})
}
defIDs = gulu.Str.RemoveDuplicatedElem(defIDs)
refCount := sql.QueryRefCount(defIDs)
for _, n := range nodes {
ast.Walk(n, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || !n.IsBlock() {
return ast.WalkContinue
}
if cnt := refCount[n.ID]; 0 < cnt {
n.SetIALAttr("refcount", strconv.Itoa(cnt))
}
return ast.WalkContinue
})
}
luteEngine := NewLute()
luteEngine.RenderOptions.ProtyleContenteditable = false // 不可编辑
dom := renderBlockDOMByNodes(nodes, luteEngine)
content := renderBlockContentByNodes(nodes)
block = &Block{Box: def.Box, Path: def.Path, HPath: b.HPath, ID: def.ID, Type: def.Type.String(), Content: dom, Markdown: content /* 这里使用 Markdown 字段来临时存储 content */}
if "" != sqlBlock.IAL {
block.IAL = map[string]string{}
ialStr := strings.TrimPrefix(sqlBlock.IAL, "{:")
ialStr = strings.TrimSuffix(ialStr, "}")
ial := parse.Tokens2IAL([]byte(ialStr))
for _, kv := range ial {
block.IAL[kv[0]] = kv[1]
}
}
if breadcrumb {
blockPaths = buildBlockBreadcrumb(def, nil, true)
}
if 1 > len(blockPaths) {
blockPaths = []*BlockPath{}
}
return
}