From 18829239d0862b0c737a9f4f892ce90330976791 Mon Sep 17 00:00:00 2001 From: Liang Ding Date: Sun, 2 Oct 2022 12:08:49 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E5=BC=8F=E5=8F=8D=E9=93=BE=E9=9D=A2=E6=9D=BF=20https://github.?= =?UTF-8?q?com/siyuan-note/siyuan/issues/3565?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kernel/api/ref.go | 29 +++++ kernel/api/router.go | 1 + kernel/model/backlink.go | 117 +++++++++++++++++++- kernel/model/path.go | 229 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 374 insertions(+), 2 deletions(-) diff --git a/kernel/api/ref.go b/kernel/api/ref.go index 29ca48289..d3ec6271e 100644 --- a/kernel/api/ref.go +++ b/kernel/api/ref.go @@ -73,6 +73,35 @@ func getBacklinkDoc(c *gin.Context) { } } + +func getBacklink2(c *gin.Context) { + ret := gulu.Ret.NewResult() + defer c.JSON(http.StatusOK, ret) + + arg, ok := util.JsonArg(c, ret) + if !ok { + return + } + + if nil == arg["id"] { + return + } + + id := arg["id"].(string) + keyword := arg["k"].(string) + mentionKeyword := arg["mk"].(string) + boxID, backlinks, backmentions, linkRefsCount, mentionsCount := model.GetBacklink2(id, keyword, mentionKeyword) + ret.Data = map[string]interface{}{ + "backlinks": backlinks, + "linkRefsCount": linkRefsCount, + "backmentions": backmentions, + "mentionsCount": mentionsCount, + "k": keyword, + "mk": mentionKeyword, + "box": boxID, + } +} + func getBacklink(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret) diff --git a/kernel/api/router.go b/kernel/api/router.go index 666ec4333..1475ac87b 100644 --- a/kernel/api/router.go +++ b/kernel/api/router.go @@ -156,6 +156,7 @@ func ServeAPI(ginServer *gin.Engine) { ginServer.Handle("POST", "/api/ref/refreshBacklink", model.CheckAuth, refreshBacklink) ginServer.Handle("POST", "/api/ref/getBacklink", model.CheckAuth, getBacklink) + ginServer.Handle("POST", "/api/ref/getBacklink2", model.CheckAuth, getBacklink2) ginServer.Handle("POST", "/api/ref/getBacklinkDoc", model.CheckAuth, getBacklinkDoc) ginServer.Handle("POST", "/api/ref/getBackmentionDoc", model.CheckAuth, getBackmentionDoc) ginServer.Handle("POST", "/api/ref/createBacklink", model.CheckAuth, model.CheckReadonly, createBacklink) diff --git a/kernel/model/backlink.go b/kernel/model/backlink.go index 65013e3d0..546604f2e 100644 --- a/kernel/model/backlink.go +++ b/kernel/model/backlink.go @@ -297,7 +297,7 @@ func buildBacklink(refID string, refTree *parse.Tree, luteEngine *lute.Lute) (re return } -func GetBacklink(id, keyword, mentionKeyword string, beforeLen int) (boxID string, backlinks, backmentions []*Path, linkRefsCount, mentionsCount int) { +func GetBacklink2(id, keyword, mentionKeyword string) (boxID string, backlinks, backmentions []*Path, linkRefsCount, mentionsCount int) { keyword = strings.TrimSpace(keyword) mentionKeyword = strings.TrimSpace(mentionKeyword) backlinks, backmentions = []*Path{}, []*Path{} @@ -323,7 +323,7 @@ func GetBacklink(id, keyword, mentionKeyword string, beforeLen int) (boxID strin backlinks = append(backlinks, l) } - mentionRefs := buildTreeBackmention(sqlBlock, linkRefs, mentionKeyword, excludeBacklinkIDs, beforeLen) + mentionRefs := buildTreeBackmention(sqlBlock, linkRefs, mentionKeyword, excludeBacklinkIDs, 12) tmpBackmentions := toFlatTree(mentionRefs, 0, "backlink") for _, l := range tmpBackmentions { l.Blocks = nil @@ -338,6 +338,119 @@ func GetBacklink(id, keyword, mentionKeyword string, beforeLen int) (boxID strin return } +func GetBacklink(id, keyword, mentionKeyword string, beforeLen int) (boxID string, linkPaths, mentionPaths []*Path, linkRefsCount, mentionsCount int) { + linkPaths = []*Path{} + mentionPaths = []*Path{} + + sqlBlock := sql.GetBlock(id) + if nil == sqlBlock { + return + } + rootID := sqlBlock.RootID + boxID = sqlBlock.Box + + var links []*Block + refs := sql.QueryRefsByDefID(id, true) + refs = removeDuplicatedRefs(refs) // 同一个块中引用多个相同块时反链去重 https://github.com/siyuan-note/siyuan/issues/3317 + + // 为了减少查询,组装好 IDs 后一次查出 + defSQLBlockIDs, refSQLBlockIDs := map[string]bool{}, map[string]bool{} + var queryBlockIDs []string + for _, ref := range refs { + defSQLBlockIDs[ref.DefBlockID] = true + refSQLBlockIDs[ref.BlockID] = true + queryBlockIDs = append(queryBlockIDs, ref.DefBlockID) + queryBlockIDs = append(queryBlockIDs, ref.BlockID) + } + querySQLBlocks := sql.GetBlocks(queryBlockIDs) + defSQLBlocksCache := map[string]*sql.Block{} + for _, defSQLBlock := range querySQLBlocks { + if nil != defSQLBlock && defSQLBlockIDs[defSQLBlock.ID] { + defSQLBlocksCache[defSQLBlock.ID] = defSQLBlock + } + } + refSQLBlocksCache := map[string]*sql.Block{} + for _, refSQLBlock := range querySQLBlocks { + if nil != refSQLBlock && refSQLBlockIDs[refSQLBlock.ID] { + refSQLBlocksCache[refSQLBlock.ID] = refSQLBlock + } + } + + excludeBacklinkIDs := hashset.New() + for _, ref := range refs { + defSQLBlock := defSQLBlocksCache[(ref.DefBlockID)] + if nil == defSQLBlock { + continue + } + + refSQLBlock := refSQLBlocksCache[ref.BlockID] + if nil == refSQLBlock { + continue + } + refBlock := fromSQLBlock(refSQLBlock, "", beforeLen) + if rootID == refBlock.RootID { // 排除当前文档内引用提及 + excludeBacklinkIDs.Add(refBlock.RootID, refBlock.ID) + } + defBlock := fromSQLBlock(defSQLBlock, "", beforeLen) + if defBlock.RootID == rootID { // 当前文档的定义块 + links = append(links, defBlock) + if ref.DefBlockID == defBlock.ID { + defBlock.Refs = append(defBlock.Refs, refBlock) + } + } + } + + for _, link := range links { + for _, ref := range link.Refs { + excludeBacklinkIDs.Add(ref.RootID, ref.ID) + } + linkRefsCount += len(link.Refs) + } + + var linkRefs []*Block + processedParagraphs := hashset.New() + var paragraphParentIDs []string + for _, link := range links { + for _, ref := range link.Refs { + if "NodeParagraph" == ref.Type { + paragraphParentIDs = append(paragraphParentIDs, ref.ParentID) + } + } + } + paragraphParents := sql.GetBlocks(paragraphParentIDs) + for _, p := range paragraphParents { + if "i" == p.Type || "h" == p.Type { + linkRefs = append(linkRefs, fromSQLBlock(p, keyword, beforeLen)) + processedParagraphs.Add(p.ID) + } + } + for _, link := range links { + for _, ref := range link.Refs { + if "NodeParagraph" == ref.Type { + if processedParagraphs.Contains(ref.ParentID) { + continue + } + } + + ref.DefID = link.ID + ref.DefPath = link.Path + + content := ref.Content + if "" != keyword { + _, content = search.MarkText(content, keyword, beforeLen, Conf.Search.CaseSensitive) + ref.Content = content + } + linkRefs = append(linkRefs, ref) + } + } + linkPaths = toSubTree(linkRefs, keyword) + + mentions := buildTreeBackmention(sqlBlock, linkRefs, mentionKeyword, excludeBacklinkIDs, beforeLen) + mentionsCount = len(mentions) + mentionPaths = toFlatTree(mentions, 0, "backlink") + return +} + func buildLinkRefs(defRootID string, refs []*sql.Ref) (ret []*Block, refsCount int, excludeBacklinkIDs *hashset.Set) { // 为了减少查询,组装好 IDs 后一次查出 defSQLBlockIDs, refSQLBlockIDs := map[string]bool{}, map[string]bool{} diff --git a/kernel/model/path.go b/kernel/model/path.go index 4f754c99d..d67b2fa64 100644 --- a/kernel/model/path.go +++ b/kernel/model/path.go @@ -27,6 +27,8 @@ import ( "github.com/88250/gulu" "github.com/88250/lute/ast" "github.com/siyuan-note/logging" + "github.com/siyuan-note/siyuan/kernel/search" + "github.com/siyuan-note/siyuan/kernel/sql" "github.com/siyuan-note/siyuan/kernel/treenode" "github.com/siyuan-note/siyuan/kernel/util" ) @@ -123,6 +125,233 @@ func toFlatTree(blocks []*Block, baseDepth int, typ string) (ret []*Path) { return } + +func toSubTree(blocks []*Block, keyword string) (ret []*Path) { + keyword = strings.TrimSpace(keyword) + var blockRoots []*Block + for _, block := range blocks { + root := getBlockIn(blockRoots, block.RootID) + if nil == root { + root, _ = getBlock(block.RootID) + blockRoots = append(blockRoots, root) + } + block.Depth = 1 + block.Count = len(block.Children) + root.Children = append(root.Children, block) + } + + for _, root := range blockRoots { + treeNode := &Path{ + ID: root.ID, + Box: root.Box, + Name: path.Base(root.HPath), + Type: "backlink", + NodeType: "NodeDocument", + SubType: root.SubType, + Depth: 0, + Count: len(root.Children), + } + for _, c := range root.Children { + if "NodeListItem" == c.Type { + tree, _ := loadTreeByBlockID(c.RootID) + li := treenode.GetNodeInTree(tree, c.ID) + if nil == li || nil == li.FirstChild { + // 反链面板拖拽到文档以后可能会出现这种情况 https://github.com/siyuan-note/siyuan/issues/5363 + continue + } + + var first *sql.Block + if 3 != li.ListData.Typ { + first = sql.GetBlock(li.FirstChild.ID) + } else { + first = sql.GetBlock(li.FirstChild.Next.ID) + } + name := first.Content + parentPos := 0 + if "" != keyword { + parentPos, name = search.MarkText(name, keyword, 12, Conf.Search.CaseSensitive) + } + subRoot := &Path{ + ID: li.ID, + Box: li.Box, + Name: name, + Type: "backlink", + NodeType: li.Type.String(), + SubType: c.SubType, + Depth: 1, + Count: 1, + } + + unfold := true + for liFirstBlockSpan := li.FirstChild.FirstChild; nil != liFirstBlockSpan; liFirstBlockSpan = liFirstBlockSpan.Next { + if treenode.IsBlockRef(liFirstBlockSpan) { + continue + } + if "" != strings.TrimSpace(liFirstBlockSpan.Text()) { + unfold = false + break + } + } + for next := li.FirstChild.Next; nil != next; next = next.Next { + subBlock, _ := getBlock(next.ID) + if unfold { + if ast.NodeList == next.Type { + for subLi := next.FirstChild; nil != subLi; subLi = subLi.Next { + subLiBlock, _ := getBlock(subLi.ID) + var subFirst *sql.Block + if 3 != subLi.ListData.Typ { + subFirst = sql.GetBlock(subLi.FirstChild.ID) + } else { + subFirst = sql.GetBlock(subLi.FirstChild.Next.ID) + } + subPos := 0 + content := subFirst.Content + if "" != keyword { + subPos, content = search.MarkText(subFirst.Content, keyword, 12, Conf.Search.CaseSensitive) + } + if -1 < subPos { + parentPos = 0 // 需要显示父级 + } + subLiBlock.Content = content + subLiBlock.Depth = 2 + subRoot.Blocks = append(subRoot.Blocks, subLiBlock) + } + } else if ast.NodeHeading == next.Type { + subBlock.Depth = 2 + subRoot.Blocks = append(subRoot.Blocks, subBlock) + headingChildren := treenode.HeadingChildren(next) + var breakSub bool + for _, n := range headingChildren { + block, _ := getBlock(n.ID) + subPos := 0 + content := block.Content + if "" != keyword { + subPos, content = search.MarkText(block.Content, keyword, 12, Conf.Search.CaseSensitive) + } + if -1 < subPos { + parentPos = 0 + } + block.Content = content + block.Depth = 3 + subRoot.Blocks = append(subRoot.Blocks, block) + if ast.NodeHeading == n.Type { + // 跳过子标题下面的块 + breakSub = true + break + } + } + if breakSub { + break + } + } else { + if nil == treenode.HeadingParent(next) { + subBlock.Depth = 2 + subRoot.Blocks = append(subRoot.Blocks, subBlock) + } + } + } + } + if -1 < parentPos { + treeNode.Children = append(treeNode.Children, subRoot) + } + } else if "NodeHeading" == c.Type { + tree, _ := loadTreeByBlockID(c.RootID) + h := treenode.GetNodeInTree(tree, c.ID) + if nil == h { + continue + } + + name := sql.GetBlock(h.ID).Content + parentPos := 0 + if "" != keyword { + parentPos, name = search.MarkText(name, keyword, 12, Conf.Search.CaseSensitive) + } + subRoot := &Path{ + ID: h.ID, + Box: h.Box, + Name: name, + Type: "backlink", + NodeType: h.Type.String(), + SubType: c.SubType, + Depth: 1, + Count: 1, + } + + unfold := true + for headingFirstSpan := h.FirstChild; nil != headingFirstSpan; headingFirstSpan = headingFirstSpan.Next { + if treenode.IsBlockRef(headingFirstSpan) { + continue + } + if "" != strings.TrimSpace(headingFirstSpan.Text()) { + unfold = false + break + } + } + + if unfold { + headingChildren := treenode.HeadingChildren(h) + for _, headingChild := range headingChildren { + if ast.NodeList == headingChild.Type { + for subLi := headingChild.FirstChild; nil != subLi; subLi = subLi.Next { + subLiBlock, _ := getBlock(subLi.ID) + var subFirst *sql.Block + if 3 != subLi.ListData.Typ { + subFirst = sql.GetBlock(subLi.FirstChild.ID) + } else { + subFirst = sql.GetBlock(subLi.FirstChild.Next.ID) + } + subPos := 0 + content := subFirst.Content + if "" != keyword { + subPos, content = search.MarkText(content, keyword, 12, Conf.Search.CaseSensitive) + } + if -1 < subPos { + parentPos = 0 + } + subLiBlock.Content = subFirst.Content + subLiBlock.Depth = 2 + subRoot.Blocks = append(subRoot.Blocks, subLiBlock) + } + } else { + subBlock, _ := getBlock(headingChild.ID) + subBlock.Depth = 2 + subRoot.Blocks = append(subRoot.Blocks, subBlock) + } + } + } + + if -1 < parentPos { + treeNode.Children = append(treeNode.Children, subRoot) + } + } else { + pos := 0 + content := c.Content + if "" != keyword { + pos, content = search.MarkText(content, keyword, 12, Conf.Search.CaseSensitive) + } + if -1 < pos { + treeNode.Blocks = append(treeNode.Blocks, c) + } + } + } + + rootPos := -1 + var rootContent string + if "" != keyword { + rootPos, rootContent = search.MarkText(treeNode.Name, keyword, 12, Conf.Search.CaseSensitive) + treeNode.Name = rootContent + } + if 0 < len(treeNode.Children) || 0 < len(treeNode.Blocks) || (-1 < rootPos && "" != keyword) { + ret = append(ret, treeNode) + } + } + + sort.Slice(ret, func(i, j int) bool { + return ret[i].ID > ret[j].ID + }) + return +} + func getBlockIn(blocks []*Block, id string) *Block { if "" == id { return nil