siyuan/kernel/model/backlink.go
2024-09-04 09:40:50 +08:00

832 lines
23 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"
"fmt"
"path"
"sort"
"strconv"
"strings"
"github.com/88250/gulu"
"github.com/88250/lute"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/emirpasic/gods/sets/hashset"
"github.com/facette/natsort"
"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"
)
func RefreshBacklink(id string) {
WaitForWritingFiles()
refreshRefsByDefID(id)
}
func refreshRefsByDefID(defID string) {
refs := sql.QueryRefsByDefID(defID, false)
trees := map[string]*parse.Tree{}
for _, ref := range refs {
tree := trees[ref.RootID]
if nil != tree {
continue
}
var loadErr error
tree, loadErr = LoadTreeByBlockID(ref.RootID)
if nil != loadErr {
logging.LogErrorf("refresh tree refs failed: %s", loadErr)
continue
}
trees[ref.RootID] = tree
sql.UpdateRefsTreeQueue(tree)
}
}
type Backlink struct {
DOM string `json:"dom"`
BlockPaths []*BlockPath `json:"blockPaths"`
Expand bool `json:"expand"`
}
func GetBackmentionDoc(defID, refTreeID, keyword string, containChildren bool) (ret []*Backlink) {
keyword = strings.TrimSpace(keyword)
ret = []*Backlink{}
beforeLen := 12
sqlBlock := sql.GetBlock(defID)
if nil == sqlBlock {
return
}
rootID := sqlBlock.RootID
refs := sql.QueryRefsByDefID(defID, containChildren)
refs = removeDuplicatedRefs(refs)
linkRefs, _, excludeBacklinkIDs := buildLinkRefs(rootID, refs, keyword)
tmpMentions, mentionKeywords := buildTreeBackmention(sqlBlock, linkRefs, keyword, excludeBacklinkIDs, beforeLen)
luteEngine := NewLute()
treeCache := map[string]*parse.Tree{}
var mentions []*Block
for _, mention := range tmpMentions {
if mention.RootID == refTreeID {
mentions = append(mentions, mention)
}
}
if "" != keyword {
mentionKeywords = append(mentionKeywords, keyword)
}
mentionKeywords = gulu.Str.RemoveDuplicatedElem(mentionKeywords)
for _, mention := range mentions {
refTree := treeCache[mention.RootID]
if nil == refTree {
var loadErr error
refTree, loadErr = LoadTreeByBlockID(mention.ID)
if nil != loadErr {
logging.LogWarnf("load ref tree [%s] failed: %s", mention.ID, loadErr)
continue
}
treeCache[mention.RootID] = refTree
}
backlink := buildBacklink(mention.ID, refTree, mentionKeywords, luteEngine)
ret = append(ret, backlink)
}
return
}
func GetBacklinkDoc(defID, refTreeID, keyword string, containChildren bool) (ret []*Backlink) {
keyword = strings.TrimSpace(keyword)
ret = []*Backlink{}
sqlBlock := sql.GetBlock(defID)
if nil == sqlBlock {
return
}
rootID := sqlBlock.RootID
tmpRefs := sql.QueryRefsByDefID(defID, containChildren)
var refs []*sql.Ref
for _, ref := range tmpRefs {
if ref.RootID == refTreeID {
refs = append(refs, ref)
}
}
refs = removeDuplicatedRefs(refs)
linkRefs, _, _ := buildLinkRefs(rootID, refs, keyword)
refTree, err := LoadTreeByBlockID(refTreeID)
if err != nil {
logging.LogWarnf("load ref tree [%s] failed: %s", refTreeID, err)
return
}
luteEngine := NewLute()
for _, linkRef := range linkRefs {
var keywords []string
if "" != keyword {
keywords = append(keywords, keyword)
}
backlink := buildBacklink(linkRef.ID, refTree, keywords, luteEngine)
ret = append(ret, backlink)
}
return
}
func buildBacklink(refID string, refTree *parse.Tree, keywords []string, luteEngine *lute.Lute) (ret *Backlink) {
n := treenode.GetNodeInTree(refTree, refID)
if nil == n {
return
}
renderNodes, expand := getBacklinkRenderNodes(n)
if 0 < len(keywords) {
for _, renderNode := range renderNodes {
var unlinks []*ast.Node
ast.Walk(renderNode, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if n.IsBlock() {
return ast.WalkContinue
}
markReplaceSpan(n, &unlinks, keywords, search.MarkDataType, luteEngine)
return ast.WalkContinue
})
for _, unlink := range unlinks {
unlink.Unlink()
}
}
}
dom := renderBlockDOMByNodes(renderNodes, luteEngine)
ret = &Backlink{
DOM: dom,
BlockPaths: buildBlockBreadcrumb(n, nil),
Expand: expand,
}
return
}
func getBacklinkRenderNodes(n *ast.Node) (ret []*ast.Node, expand bool) {
expand = true
if ast.NodeListItem == n.Type {
if nil == n.FirstChild {
return
}
c := n.FirstChild
if 3 == n.ListData.Typ {
c = n.FirstChild.Next
}
if c != n.LastChild { // 存在子列表
for liFirstBlockSpan := c.FirstChild; nil != liFirstBlockSpan; liFirstBlockSpan = liFirstBlockSpan.Next {
if treenode.IsBlockRef(liFirstBlockSpan) {
continue
}
if "" != strings.TrimSpace(liFirstBlockSpan.Text()) {
expand = false
break
}
}
}
ret = append(ret, n)
} else if ast.NodeHeading == n.Type {
c := n.FirstChild
if nil == c {
return
}
for headingFirstSpan := c; nil != headingFirstSpan; headingFirstSpan = headingFirstSpan.Next {
if treenode.IsBlockRef(headingFirstSpan) {
continue
}
if "" != strings.TrimSpace(headingFirstSpan.Text()) {
expand = false
break
}
}
ret = append(ret, n)
cc := treenode.HeadingChildren(n)
ret = append(ret, cc...)
} else {
ret = append(ret, n)
}
return
}
func GetBacklink2(id, keyword, mentionKeyword string, sortMode, mentionSortMode int) (boxID string, backlinks, backmentions []*Path, linkRefsCount, mentionsCount int) {
keyword = strings.TrimSpace(keyword)
mentionKeyword = strings.TrimSpace(mentionKeyword)
backlinks, backmentions = []*Path{}, []*Path{}
sqlBlock := sql.GetBlock(id)
if nil == sqlBlock {
return
}
rootID := sqlBlock.RootID
boxID = sqlBlock.Box
refs := sql.QueryRefsByDefID(id, true)
refs = removeDuplicatedRefs(refs)
linkRefs, linkRefsCount, excludeBacklinkIDs := buildLinkRefs(rootID, refs, keyword)
tmpBacklinks := toFlatTree(linkRefs, 0, "backlink", nil)
for _, l := range tmpBacklinks {
l.Blocks = nil
backlinks = append(backlinks, l)
}
sort.Slice(backlinks, func(i, j int) bool {
switch sortMode {
case util.SortModeUpdatedDESC:
return backlinks[i].Updated > backlinks[j].Updated
case util.SortModeUpdatedASC:
return backlinks[i].Updated < backlinks[j].Updated
case util.SortModeCreatedDESC:
return backlinks[i].Created > backlinks[j].Created
case util.SortModeCreatedASC:
return backlinks[i].Created < backlinks[j].Created
case util.SortModeNameDESC:
return util.PinYinCompare(util.RemoveEmojiInvisible(backlinks[j].Name), util.RemoveEmojiInvisible(backlinks[i].Name))
case util.SortModeNameASC:
return util.PinYinCompare(util.RemoveEmojiInvisible(backlinks[i].Name), util.RemoveEmojiInvisible(backlinks[j].Name))
case util.SortModeAlphanumDESC:
return natsort.Compare(util.RemoveEmojiInvisible(backlinks[j].Name), util.RemoveEmojiInvisible(backlinks[i].Name))
case util.SortModeAlphanumASC:
return natsort.Compare(util.RemoveEmojiInvisible(backlinks[i].Name), util.RemoveEmojiInvisible(backlinks[j].Name))
}
return backlinks[i].ID > backlinks[j].ID
})
mentionRefs, _ := buildTreeBackmention(sqlBlock, linkRefs, mentionKeyword, excludeBacklinkIDs, 12)
tmpBackmentions := toFlatTree(mentionRefs, 0, "backlink", nil)
for _, l := range tmpBackmentions {
l.Blocks = nil
backmentions = append(backmentions, l)
}
sort.Slice(backmentions, func(i, j int) bool {
switch mentionSortMode {
case util.SortModeUpdatedDESC:
return backmentions[i].Updated > backmentions[j].Updated
case util.SortModeUpdatedASC:
return backmentions[i].Updated < backmentions[j].Updated
case util.SortModeCreatedDESC:
return backmentions[i].Created > backmentions[j].Created
case util.SortModeCreatedASC:
return backmentions[i].Created < backmentions[j].Created
case util.SortModeNameDESC:
return util.PinYinCompare(util.RemoveEmojiInvisible(backmentions[j].Name), util.RemoveEmojiInvisible(backmentions[i].Name))
case util.SortModeNameASC:
return util.PinYinCompare(util.RemoveEmojiInvisible(backmentions[i].Name), util.RemoveEmojiInvisible(backmentions[j].Name))
case util.SortModeAlphanumDESC:
return natsort.Compare(util.RemoveEmojiInvisible(backmentions[j].Name), util.RemoveEmojiInvisible(backmentions[i].Name))
case util.SortModeAlphanumASC:
return natsort.Compare(util.RemoveEmojiInvisible(backmentions[i].Name), util.RemoveEmojiInvisible(backmentions[j].Name))
}
return backmentions[i].ID > backmentions[j].ID
})
for _, backmention := range backmentions {
mentionsCount += backmention.Count
}
// 添加笔记本名称
var boxIDs []string
for _, l := range backlinks {
boxIDs = append(boxIDs, l.Box)
}
for _, l := range backmentions {
boxIDs = append(boxIDs, l.Box)
}
boxIDs = gulu.Str.RemoveDuplicatedElem(boxIDs)
boxNames := Conf.BoxNames(boxIDs)
for _, l := range backlinks {
name := boxNames[l.Box]
l.HPath = name + l.HPath
}
for _, l := range backmentions {
name := boxNames[l.Box]
l.HPath = name + l.HPath
}
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)
// 为了减少查询,组装好 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", nil)
return
}
func buildLinkRefs(defRootID string, refs []*sql.Ref, keyword string) (ret []*Block, refsCount int, excludeBacklinkIDs *hashset.Set) {
// 为了减少查询,组装好 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)
}
queryBlockIDs = gulu.Str.RemoveDuplicatedElem(queryBlockIDs)
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
}
}
var links []*Block
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, "", 12)
if defRootID == refBlock.RootID { // 排除当前文档内引用提及
excludeBacklinkIDs.Add(refBlock.RootID, refBlock.ID)
}
defBlock := fromSQLBlock(defSQLBlock, "", 12)
if defBlock.RootID == defRootID { // 当前文档的定义块
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)
}
refsCount += len(link.Refs)
}
parentRefParagraphs := map[string]*Block{}
for _, link := range links {
for _, ref := range link.Refs {
if "NodeParagraph" == ref.Type {
parentRefParagraphs[ref.ParentID] = ref
}
}
}
var paragraphParentIDs []string
for parentID, _ := range parentRefParagraphs {
paragraphParentIDs = append(paragraphParentIDs, parentID)
}
sqlParagraphParents := sql.GetBlocks(paragraphParentIDs)
paragraphParents := fromSQLBlocks(&sqlParagraphParents, "", 12)
processedParagraphs := hashset.New()
for _, p := range paragraphParents {
// 改进标题下方块和列表项子块引用时的反链定位 https://github.com/siyuan-note/siyuan/issues/7484
if "NodeListItem" == p.Type {
refBlock := parentRefParagraphs[p.ID]
if nil != refBlock && p.FContent == refBlock.Content { // 使用内容判断是否是列表项下第一个子块
// 如果是列表项下第一个子块,则后续会通过列表项传递或关联处理,所以这里就不处理这个段落了
processedParagraphs.Add(p.ID)
if !strings.Contains(p.Content, keyword) {
refsCount--
continue
}
ret = append(ret, p)
}
}
}
for _, link := range links {
for _, ref := range link.Refs {
if "NodeParagraph" == ref.Type {
if processedParagraphs.Contains(ref.ParentID) {
continue
}
}
if !strings.Contains(ref.Content, keyword) {
refsCount--
continue
}
ref.DefID = link.ID
ref.DefPath = link.Path
ret = append(ret, ref)
}
}
return
}
func removeDuplicatedRefs(refs []*sql.Ref) (ret []*sql.Ref) {
// 同一个块中引用多个块后反链去重
// De-duplication of backlinks after referencing multiple blocks in the same block https://github.com/siyuan-note/siyuan/issues/12147
for _, ref := range refs {
contain := false
for _, r := range ret {
if ref.BlockID == r.BlockID {
contain = true
break
}
}
if !contain {
ret = append(ret, ref)
}
}
return
}
func buildTreeBackmention(defSQLBlock *sql.Block, refBlocks []*Block, keyword string, excludeBacklinkIDs *hashset.Set, beforeLen int) (ret []*Block, mentionKeywords []string) {
ret = []*Block{}
var names, aliases []string
var fName, rootID string
if "d" == defSQLBlock.Type {
if Conf.Search.BacklinkMentionName {
names = sql.QueryBlockNamesByRootID(defSQLBlock.ID)
}
if Conf.Search.BacklinkMentionAlias {
aliases = sql.QueryBlockAliases(defSQLBlock.ID)
}
if Conf.Search.BacklinkMentionDoc {
fName = path.Base(defSQLBlock.HPath)
}
rootID = defSQLBlock.ID
} else {
if Conf.Search.BacklinkMentionName {
if "" != defSQLBlock.Name {
names = append(names, defSQLBlock.Name)
}
}
if Conf.Search.BacklinkMentionAlias {
if "" != defSQLBlock.Alias {
aliases = strings.Split(defSQLBlock.Alias, ",")
}
}
root := treenode.GetBlockTree(defSQLBlock.RootID)
rootID = root.ID
}
set := hashset.New()
for _, name := range names {
set.Add(name)
}
for _, alias := range aliases {
set.Add(alias)
}
if "" != fName {
set.Add(fName)
}
if Conf.Search.BacklinkMentionAnchor {
for _, refBlock := range refBlocks {
refs := sql.QueryRefsByDefIDRefID(refBlock.DefID, refBlock.ID)
for _, ref := range refs {
set.Add(ref.Content)
}
}
}
for _, v := range set.Values() {
mentionKeywords = append(mentionKeywords, v.(string))
}
mentionKeywords = prepareMarkKeywords(mentionKeywords)
ret = searchBackmention(mentionKeywords, keyword, excludeBacklinkIDs, rootID, beforeLen)
return
}
func searchBackmention(mentionKeywords []string, keyword string, excludeBacklinkIDs *hashset.Set, rootID string, beforeLen int) (ret []*Block) {
ret = []*Block{}
if 1 > len(mentionKeywords) {
return
}
table := "blocks_fts" // 大小写敏感
if !Conf.Search.CaseSensitive {
table = "blocks_fts_case_insensitive"
}
buf := bytes.Buffer{}
buf.WriteString("SELECT * FROM " + table + " WHERE " + table + " MATCH '" + columnFilter() + ":(")
for i, mentionKeyword := range mentionKeywords {
if Conf.Search.BacklinkMentionKeywordsLimit < i {
util.PushMsg(fmt.Sprintf(Conf.Language(38), len(mentionKeywords)), 5000)
mentionKeyword = strings.ReplaceAll(mentionKeyword, "\"", "\"\"")
buf.WriteString("\"" + mentionKeyword + "\"")
break
}
mentionKeyword = strings.ReplaceAll(mentionKeyword, "\"", "\"\"")
buf.WriteString("\"" + mentionKeyword + "\"")
if i < len(mentionKeywords)-1 {
buf.WriteString(" OR ")
}
}
buf.WriteString(")")
if "" != keyword {
keyword = strings.ReplaceAll(keyword, "\"", "\"\"")
buf.WriteString(" AND (\"" + keyword + "\")")
}
buf.WriteString("'")
buf.WriteString(" AND root_id != '" + rootID + "'") // 不在定义块所在文档中搜索
buf.WriteString(" AND type IN ('d', 'h', 'p', 't')")
buf.WriteString(" ORDER BY id DESC LIMIT " + strconv.Itoa(Conf.Search.Limit))
query := buf.String()
sqlBlocks := sql.SelectBlocksRawStmt(query, 1, Conf.Search.Limit)
terms := mentionKeywords
if "" != keyword {
terms = append(terms, keyword)
}
blocks := fromSQLBlocks(&sqlBlocks, strings.Join(terms, search.TermSep), beforeLen)
luteEngine := util.NewLute()
var tmp []*Block
for _, b := range blocks {
tree := parse.Parse("", gulu.Str.ToBytes(b.Markdown), luteEngine.ParseOptions)
if nil == tree {
continue
}
textBuf := &bytes.Buffer{}
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || n.IsBlock() {
return ast.WalkContinue
}
if ast.NodeText == n.Type { // 这里包含了标签命中的情况,因为 Lute 没有启用 TextMark
textBuf.Write(n.Tokens)
}
return ast.WalkContinue
})
text := textBuf.String()
text = strings.TrimSpace(text)
if "" == text {
continue
}
newText := markReplaceSpanWithSplit(text, mentionKeywords, search.GetMarkSpanStart(search.MarkDataType), search.GetMarkSpanEnd())
if text != newText {
tmp = append(tmp, b)
} else {
// columnFilter 中的命名、别名和备注命中的情况
// 反链提及搜索范围增加命名、别名和备注 https://github.com/siyuan-note/siyuan/issues/7639
if gulu.Str.Contains(trimMarkTags(b.Name), mentionKeywords) ||
gulu.Str.Contains(trimMarkTags(b.Alias), mentionKeywords) ||
gulu.Str.Contains(trimMarkTags(b.Memo), mentionKeywords) {
tmp = append(tmp, b)
}
}
}
blocks = tmp
mentionBlockMap := map[string]*Block{}
for _, block := range blocks {
mentionBlockMap[block.ID] = block
refText := getContainStr(block.Content, mentionKeywords)
block.RefText = refText
}
for _, mentionBlock := range mentionBlockMap {
if !excludeBacklinkIDs.Contains(mentionBlock.ID) {
ret = append(ret, mentionBlock)
}
}
sort.SliceStable(ret, func(i, j int) bool {
return ret[i].ID > ret[j].ID
})
return
}
func trimMarkTags(str string) string {
return strings.TrimSuffix(strings.TrimPrefix(str, "<mark>"), "</mark>")
}
func getContainStr(str string, strs []string) string {
str = strings.ToLower(str)
for _, s := range strs {
if strings.Contains(str, strings.ToLower(s)) {
return s
}
}
return ""
}
// buildFullLinks 构建正向和反向链接列表。
// forwardlinks正向链接关系 refs
// backlinks反向链接关系 defs
func buildFullLinks(condition string) (forwardlinks, backlinks []*Block) {
forwardlinks, backlinks = []*Block{}, []*Block{}
defs := buildDefsAndRefs(condition)
backlinks = append(backlinks, defs...)
for _, def := range defs {
for _, ref := range def.Refs {
forwardlinks = append(forwardlinks, ref)
}
}
return
}
func buildDefsAndRefs(condition string) (defBlocks []*Block) {
defBlockMap := map[string]*Block{}
refBlockMap := map[string]*Block{}
defRefs := sql.DefRefs(condition)
// 将 sql block 转为 block
for _, row := range defRefs {
for def, ref := range row {
if nil == ref {
continue
}
refBlock := refBlockMap[ref.ID]
if nil == refBlock {
refBlock = fromSQLBlock(ref, "", 0)
refBlockMap[ref.ID] = refBlock
}
// ref 块自己也需要作为定义块,否则图上没有节点
if defBlock := defBlockMap[ref.ID]; nil == defBlock {
defBlockMap[ref.ID] = refBlock
}
if defBlock := defBlockMap[def.ID]; nil == defBlock {
defBlock = fromSQLBlock(def, "", 0)
defBlockMap[def.ID] = defBlock
}
}
}
// 组装 block.Defs 和 block.Refs 字段
for _, row := range defRefs {
for def, ref := range row {
if nil == ref {
defBlock := fromSQLBlock(def, "", 0)
defBlockMap[def.ID] = defBlock
continue
}
refBlock := refBlockMap[ref.ID]
defBlock := defBlockMap[def.ID]
if refBlock.ID == defBlock.ID { // 自引用
continue
}
refBlock.Defs = append(refBlock.Defs, defBlock)
defBlock.Refs = append(defBlock.Refs, refBlock)
}
}
for _, def := range defBlockMap {
defBlocks = append(defBlocks, def)
}
return
}