832 lines
23 KiB
Go
832 lines
23 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 (
|
||
"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
|
||
}
|