123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829 |
- // 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) (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, true)
- refs = removeDuplicatedRefs(refs) // 同一个块中引用多个相同块时反链去重 https://github.com/siyuan-note/siyuan/issues/3317
- 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) (ret []*Backlink) {
- keyword = strings.TrimSpace(keyword)
- ret = []*Backlink{}
- sqlBlock := sql.GetBlock(defID)
- if nil == sqlBlock {
- return
- }
- rootID := sqlBlock.RootID
- tmpRefs := sql.QueryRefsByDefID(defID, true)
- var refs []*sql.Ref
- for _, ref := range tmpRefs {
- if ref.RootID == refTreeID {
- refs = append(refs, ref)
- }
- }
- refs = removeDuplicatedRefs(refs) // 同一个块中引用多个相同块时反链去重 https://github.com/siyuan-note/siyuan/issues/3317
- linkRefs, _, _ := buildLinkRefs(rootID, refs, keyword)
- refTree, err := LoadTreeByBlockID(refTreeID)
- if nil != err {
- 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) // 同一个块中引用多个相同块时反链去重 https://github.com/siyuan-note/siyuan/issues/3317
- 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) // 同一个块中引用多个相同块时反链去重 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", 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) {
- for _, ref := range refs {
- contain := false
- for _, r := range ret {
- if ref.DefBlockID == r.DefBlockID && 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
- }
|