566 lines
15 KiB
Go
566 lines
15 KiB
Go
// 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 (
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/88250/gulu"
|
|
"github.com/88250/lute/ast"
|
|
"github.com/88250/lute/editor"
|
|
"github.com/88250/lute/parse"
|
|
"github.com/siyuan-note/logging"
|
|
"github.com/siyuan-note/siyuan/kernel/av"
|
|
"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"
|
|
)
|
|
|
|
type BlockInfo struct {
|
|
ID string `json:"id"`
|
|
RootID string `json:"rootID"`
|
|
Name string `json:"name"`
|
|
RefCount int `json:"refCount"`
|
|
SubFileCount int `json:"subFileCount"`
|
|
RefIDs []string `json:"refIDs"`
|
|
IAL map[string]string `json:"ial"`
|
|
Icon string `json:"icon"`
|
|
AttrViews []*AttrView `json:"attrViews"`
|
|
}
|
|
|
|
type AttrView struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func GetDocInfo(blockID string) (ret *BlockInfo) {
|
|
FlushTxQueue()
|
|
|
|
tree, err := LoadTreeByBlockID(blockID)
|
|
if err != nil {
|
|
logging.LogErrorf("load tree by root id [%s] failed: %s", blockID, err)
|
|
return
|
|
}
|
|
|
|
title := tree.Root.IALAttr("title")
|
|
ret = &BlockInfo{ID: blockID, RootID: tree.Root.ID, Name: title}
|
|
ret.IAL = parse.IAL2Map(tree.Root.KramdownIAL)
|
|
scrollData := ret.IAL["scroll"]
|
|
if 0 < len(scrollData) {
|
|
scroll := map[string]interface{}{}
|
|
if parseErr := gulu.JSON.UnmarshalJSON([]byte(scrollData), &scroll); nil != parseErr {
|
|
logging.LogWarnf("parse scroll data [%s] failed: %s", scrollData, parseErr)
|
|
delete(ret.IAL, "scroll")
|
|
} else {
|
|
if zoomInId := scroll["zoomInId"]; nil != zoomInId {
|
|
if !treenode.ExistBlockTree(zoomInId.(string)) {
|
|
delete(ret.IAL, "scroll")
|
|
}
|
|
} else {
|
|
if startId := scroll["startId"]; nil != startId {
|
|
if !treenode.ExistBlockTree(startId.(string)) {
|
|
delete(ret.IAL, "scroll")
|
|
}
|
|
}
|
|
if endId := scroll["endId"]; nil != endId {
|
|
if !treenode.ExistBlockTree(endId.(string)) {
|
|
delete(ret.IAL, "scroll")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ret.RefIDs, _ = sql.QueryRefIDsByDefID(blockID, Conf.Editor.BacklinkContainChildren)
|
|
buildBacklinkListItemRefs(&ret.RefIDs)
|
|
ret.RefCount = len(ret.RefIDs) // 填充块引计数
|
|
|
|
// 填充属性视图角标 Display the database title on the block superscript https://github.com/siyuan-note/siyuan/issues/10545
|
|
avIDs := strings.Split(ret.IAL[av.NodeAttrNameAvs], ",")
|
|
for _, avID := range avIDs {
|
|
avName, getErr := av.GetAttributeViewName(avID)
|
|
if nil != getErr {
|
|
continue
|
|
}
|
|
|
|
if "" == avName {
|
|
avName = Conf.language(105)
|
|
}
|
|
|
|
attrView := &AttrView{ID: avID, Name: avName}
|
|
ret.AttrViews = append(ret.AttrViews, attrView)
|
|
}
|
|
|
|
var subFileCount int
|
|
boxLocalPath := filepath.Join(util.DataDir, tree.Box)
|
|
subFiles, err := os.ReadDir(filepath.Join(boxLocalPath, strings.TrimSuffix(tree.Path, ".sy")))
|
|
if err == nil {
|
|
for _, subFile := range subFiles {
|
|
if strings.HasSuffix(subFile.Name(), ".sy") {
|
|
subFileCount++
|
|
}
|
|
}
|
|
}
|
|
ret.SubFileCount = subFileCount
|
|
ret.Icon = tree.Root.IALAttr("icon")
|
|
return
|
|
}
|
|
|
|
func GetDocsInfo(blockIDs []string, queryRefCount bool, queryAv bool) (rets []*BlockInfo) {
|
|
FlushTxQueue()
|
|
|
|
trees := filesys.LoadTrees(blockIDs)
|
|
for _, blockID := range blockIDs {
|
|
tree := trees[blockID]
|
|
if nil == tree {
|
|
continue
|
|
}
|
|
title := tree.Root.IALAttr("title")
|
|
ret := &BlockInfo{ID: blockID, RootID: tree.Root.ID, Name: title}
|
|
ret.IAL = parse.IAL2Map(tree.Root.KramdownIAL)
|
|
scrollData := ret.IAL["scroll"]
|
|
if 0 < len(scrollData) {
|
|
scroll := map[string]interface{}{}
|
|
if parseErr := gulu.JSON.UnmarshalJSON([]byte(scrollData), &scroll); nil != parseErr {
|
|
logging.LogWarnf("parse scroll data [%s] failed: %s", scrollData, parseErr)
|
|
delete(ret.IAL, "scroll")
|
|
} else {
|
|
if zoomInId := scroll["zoomInId"]; nil != zoomInId {
|
|
if !treenode.ExistBlockTree(zoomInId.(string)) {
|
|
delete(ret.IAL, "scroll")
|
|
}
|
|
} else {
|
|
if startId := scroll["startId"]; nil != startId {
|
|
if !treenode.ExistBlockTree(startId.(string)) {
|
|
delete(ret.IAL, "scroll")
|
|
}
|
|
}
|
|
if endId := scroll["endId"]; nil != endId {
|
|
if !treenode.ExistBlockTree(endId.(string)) {
|
|
delete(ret.IAL, "scroll")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if queryRefCount {
|
|
ret.RefIDs, _ = sql.QueryRefIDsByDefID(blockID, Conf.Editor.BacklinkContainChildren)
|
|
ret.RefCount = len(ret.RefIDs) // 填充块引计数
|
|
}
|
|
|
|
if queryAv {
|
|
// 填充属性视图角标 Display the database title on the block superscript https://github.com/siyuan-note/siyuan/issues/10545
|
|
avIDs := strings.Split(ret.IAL[av.NodeAttrNameAvs], ",")
|
|
for _, avID := range avIDs {
|
|
avName, getErr := av.GetAttributeViewName(avID)
|
|
if nil != getErr {
|
|
continue
|
|
}
|
|
|
|
if "" == avName {
|
|
avName = Conf.language(105)
|
|
}
|
|
|
|
attrView := &AttrView{ID: avID, Name: avName}
|
|
ret.AttrViews = append(ret.AttrViews, attrView)
|
|
}
|
|
}
|
|
|
|
var subFileCount int
|
|
boxLocalPath := filepath.Join(util.DataDir, tree.Box)
|
|
subFiles, err := os.ReadDir(filepath.Join(boxLocalPath, strings.TrimSuffix(tree.Path, ".sy")))
|
|
if err == nil {
|
|
for _, subFile := range subFiles {
|
|
if strings.HasSuffix(subFile.Name(), ".sy") {
|
|
subFileCount++
|
|
}
|
|
}
|
|
}
|
|
ret.SubFileCount = subFileCount
|
|
ret.Icon = tree.Root.IALAttr("icon")
|
|
|
|
rets = append(rets, ret)
|
|
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetBlockRefText(id string) string {
|
|
FlushTxQueue()
|
|
|
|
bt := treenode.GetBlockTree(id)
|
|
if nil == bt {
|
|
return ErrBlockNotFound.Error()
|
|
}
|
|
|
|
tree, err := LoadTreeByBlockID(id)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
node := treenode.GetNodeInTree(tree, id)
|
|
if nil == node {
|
|
return ErrBlockNotFound.Error()
|
|
}
|
|
|
|
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
if n.IsTextMarkType("inline-memo") {
|
|
// Block ref anchor text no longer contains contents of inline-level memos https://github.com/siyuan-note/siyuan/issues/9363
|
|
n.TextMarkInlineMemoContent = ""
|
|
return ast.WalkContinue
|
|
}
|
|
return ast.WalkContinue
|
|
})
|
|
return getNodeRefText(node)
|
|
}
|
|
|
|
func GetDOMText(dom string) (ret string) {
|
|
luteEngine := NewLute()
|
|
tree := luteEngine.BlockDOM2Tree(dom)
|
|
ret = renderBlockText(tree.Root.FirstChild, nil)
|
|
return
|
|
}
|
|
|
|
func getBlockRefText(id string, tree *parse.Tree) (ret string) {
|
|
node := treenode.GetNodeInTree(tree, id)
|
|
if nil == node {
|
|
return
|
|
}
|
|
|
|
ret = getNodeRefText(node)
|
|
ret = maxContent(ret, Conf.Editor.BlockRefDynamicAnchorTextMaxLen)
|
|
return
|
|
}
|
|
|
|
func getNodeRefText(node *ast.Node) string {
|
|
if nil == node {
|
|
return ""
|
|
}
|
|
|
|
if ret := node.IALAttr("name"); "" != ret {
|
|
ret = strings.TrimSpace(ret)
|
|
ret = util.EscapeHTML(ret)
|
|
return ret
|
|
}
|
|
return getNodeRefText0(node, Conf.Editor.BlockRefDynamicAnchorTextMaxLen)
|
|
}
|
|
|
|
func getNodeAvBlockText(node *ast.Node) (icon, content string) {
|
|
if nil == node {
|
|
return
|
|
}
|
|
|
|
icon = node.IALAttr("icon")
|
|
if name := node.IALAttr("name"); "" != name {
|
|
name = strings.TrimSpace(name)
|
|
name = util.EscapeHTML(name)
|
|
content = name
|
|
} else {
|
|
content = getNodeRefText0(node, 1024)
|
|
}
|
|
return
|
|
}
|
|
|
|
func getNodeRefText0(node *ast.Node, maxLen int) string {
|
|
switch node.Type {
|
|
case ast.NodeBlockQueryEmbed:
|
|
return "Query Embed Block..."
|
|
case ast.NodeIFrame:
|
|
return "IFrame..."
|
|
case ast.NodeThematicBreak:
|
|
return "Thematic Break..."
|
|
case ast.NodeVideo:
|
|
return "Video..."
|
|
case ast.NodeAudio:
|
|
return "Audio..."
|
|
case ast.NodeAttributeView:
|
|
ret, _ := av.GetAttributeViewName(node.AttributeViewID)
|
|
if "" == ret {
|
|
ret = "Database..."
|
|
}
|
|
return ret
|
|
}
|
|
|
|
if ast.NodeDocument != node.Type && node.IsContainerBlock() {
|
|
node = treenode.FirstLeafBlock(node)
|
|
}
|
|
ret := renderBlockText(node, nil)
|
|
if maxLen < utf8.RuneCountInString(ret) {
|
|
ret = gulu.Str.SubStr(ret, maxLen) + "..."
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func GetBlockRefs(defID string, isBacklink bool) (refIDs, refTexts, defIDs []string) {
|
|
refIDs = []string{}
|
|
refTexts = []string{}
|
|
defIDs = []string{}
|
|
bt := treenode.GetBlockTree(defID)
|
|
if nil == bt {
|
|
return
|
|
}
|
|
|
|
isDoc := bt.ID == bt.RootID
|
|
refIDs, refTexts = sql.QueryRefIDsByDefID(defID, isDoc)
|
|
if isDoc {
|
|
defIDs = sql.QueryChildDefIDsByRootDefID(defID)
|
|
} else {
|
|
defIDs = append(defIDs, defID)
|
|
}
|
|
|
|
if isBacklink {
|
|
buildBacklinkListItemRefs(&refIDs)
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetBlockRefIDsByFileAnnotationID(id string) (refIDs, refTexts []string) {
|
|
refIDs, refTexts = sql.QueryRefIDsByAnnotationID(id)
|
|
return
|
|
}
|
|
|
|
func GetBlockDefIDsByRefText(refText string, excludeIDs []string) (ret []string) {
|
|
ret = sql.QueryBlockDefIDsByRefText(refText, excludeIDs)
|
|
sort.Sort(sort.Reverse(sort.StringSlice(ret)))
|
|
if 1 > len(ret) {
|
|
ret = []string{}
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetBlockIndex(id string) (ret int) {
|
|
tree, _ := LoadTreeByBlockID(id)
|
|
if nil == tree {
|
|
return
|
|
}
|
|
node := treenode.GetNodeInTree(tree, id)
|
|
if nil == node {
|
|
return
|
|
}
|
|
|
|
rootChild := node
|
|
for ; nil != rootChild.Parent && ast.NodeDocument != rootChild.Parent.Type; rootChild = rootChild.Parent {
|
|
}
|
|
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
if !n.IsChildBlockOf(tree.Root, 1) {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
ret++
|
|
if n.ID == rootChild.ID {
|
|
return ast.WalkStop
|
|
}
|
|
return ast.WalkContinue
|
|
})
|
|
return
|
|
}
|
|
|
|
func GetBlocksIndexes(ids []string) (ret map[string]int) {
|
|
ret = map[string]int{}
|
|
if 1 > len(ids) {
|
|
return
|
|
}
|
|
|
|
tree, _ := LoadTreeByBlockID(ids[0])
|
|
if nil == tree {
|
|
return
|
|
}
|
|
|
|
idx := 0
|
|
nodesIndexes := map[string]int{}
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
if !n.IsChildBlockOf(tree.Root, 1) {
|
|
if n.IsBlock() {
|
|
nodesIndexes[n.ID] = idx
|
|
}
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
idx++
|
|
nodesIndexes[n.ID] = idx
|
|
return ast.WalkContinue
|
|
})
|
|
|
|
for _, id := range ids {
|
|
ret[id] = nodesIndexes[id]
|
|
}
|
|
return
|
|
}
|
|
|
|
type BlockPath struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
SubType string `json:"subType"`
|
|
Children []*BlockPath `json:"children"`
|
|
}
|
|
|
|
func BuildBlockBreadcrumb(id string, excludeTypes []string) (ret []*BlockPath, err error) {
|
|
ret = []*BlockPath{}
|
|
tree, err := LoadTreeByBlockID(id)
|
|
if nil == tree {
|
|
err = nil
|
|
return
|
|
}
|
|
node := treenode.GetNodeInTree(tree, id)
|
|
if nil == node {
|
|
return
|
|
}
|
|
|
|
ret = buildBlockBreadcrumb(node, excludeTypes, false)
|
|
return
|
|
}
|
|
|
|
func buildBlockBreadcrumb(node *ast.Node, excludeTypes []string, isEmbedBlock bool) (ret []*BlockPath) {
|
|
ret = []*BlockPath{}
|
|
if nil == node {
|
|
return
|
|
}
|
|
box := Conf.Box(node.Box)
|
|
if nil == box {
|
|
return
|
|
}
|
|
|
|
headingLevel := 16
|
|
maxNameLen := 1024
|
|
var hPath string
|
|
baseBlock := treenode.GetBlockTreeRootByPath(node.Box, node.Path)
|
|
if nil != baseBlock {
|
|
hPath = baseBlock.HPath
|
|
}
|
|
for parent := node; nil != parent; parent = parent.Parent {
|
|
if "" == parent.ID {
|
|
continue
|
|
}
|
|
id := parent.ID
|
|
fc := treenode.FirstLeafBlock(parent)
|
|
|
|
name := parent.IALAttr("name")
|
|
if ast.NodeDocument == parent.Type {
|
|
name = box.Name + hPath
|
|
} else if ast.NodeAttributeView == parent.Type {
|
|
name, _ = av.GetAttributeViewName(parent.AttributeViewID)
|
|
} else {
|
|
if "" == name {
|
|
if ast.NodeListItem == parent.Type || ast.NodeList == parent.Type || ast.NodeSuperBlock == parent.Type || ast.NodeBlockquote == parent.Type {
|
|
name = gulu.Str.SubStr(renderBlockText(fc, excludeTypes), maxNameLen)
|
|
} else {
|
|
name = gulu.Str.SubStr(renderBlockText(parent, excludeTypes), maxNameLen)
|
|
}
|
|
}
|
|
if ast.NodeHeading == parent.Type {
|
|
headingLevel = parent.HeadingLevel
|
|
}
|
|
}
|
|
|
|
add := true
|
|
if ast.NodeList == parent.Type || ast.NodeSuperBlock == parent.Type || ast.NodeBlockquote == parent.Type {
|
|
add = false
|
|
if parent == node {
|
|
// https://github.com/siyuan-note/siyuan/issues/13141#issuecomment-2476789553
|
|
add = true
|
|
}
|
|
}
|
|
if ast.NodeParagraph == parent.Type && nil != parent.Parent && ast.NodeListItem == parent.Parent.Type && nil == parent.Next && (nil == parent.Previous || ast.NodeTaskListItemMarker == parent.Previous.Type) {
|
|
add = false
|
|
}
|
|
if ast.NodeListItem == parent.Type {
|
|
if "" == name {
|
|
name = gulu.Str.SubStr(renderBlockText(fc, excludeTypes), maxNameLen)
|
|
}
|
|
}
|
|
|
|
name = strings.ReplaceAll(name, editor.Caret, "")
|
|
name = util.UnescapeHTML(name)
|
|
name = util.EscapeHTML(name)
|
|
|
|
if !isEmbedBlock && parent == node {
|
|
name = ""
|
|
}
|
|
|
|
if add {
|
|
ret = append([]*BlockPath{{
|
|
ID: id,
|
|
Name: name,
|
|
Type: parent.Type.String(),
|
|
SubType: treenode.SubTypeAbbr(parent),
|
|
}}, ret...)
|
|
}
|
|
|
|
for prev := parent.Previous; nil != prev; prev = prev.Previous {
|
|
b := prev
|
|
if ast.NodeSuperBlock == prev.Type {
|
|
// 超级块中包含标题块时下方块面包屑计算不正确 https://github.com/siyuan-note/siyuan/issues/6675
|
|
b = treenode.SuperBlockLastHeading(prev)
|
|
if nil == b {
|
|
// 超级块下方块被作为嵌入块时设置显示面包屑后不渲染 https://github.com/siyuan-note/siyuan/issues/6690
|
|
b = prev
|
|
}
|
|
}
|
|
|
|
if ast.NodeHeading == b.Type && headingLevel > b.HeadingLevel {
|
|
if b.ParentIs(ast.NodeListItem) {
|
|
// 标题在列表下时不显示 https://github.com/siyuan-note/siyuan/issues/13008
|
|
continue
|
|
}
|
|
|
|
name = gulu.Str.SubStr(renderBlockText(b, excludeTypes), maxNameLen)
|
|
name = util.EscapeHTML(name)
|
|
ret = append([]*BlockPath{{
|
|
ID: b.ID,
|
|
Name: name,
|
|
Type: b.Type.String(),
|
|
SubType: treenode.SubTypeAbbr(b),
|
|
}}, ret...)
|
|
headingLevel = b.HeadingLevel
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func buildBacklinkListItemRefs(refIDs *[]string) {
|
|
refBts := treenode.GetBlockTrees(*refIDs)
|
|
for i, refID := range *refIDs {
|
|
if bt := refBts[refID]; nil != bt {
|
|
if "p" == bt.Type {
|
|
if parent := treenode.GetBlockTree(bt.ParentID); nil != parent && "i" == parent.Type {
|
|
// 引用计数浮窗请求,需要按照反链逻辑组装 https://github.com/siyuan-note/siyuan/issues/6853
|
|
(*refIDs)[i] = parent.ID
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|