backlink.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. // SiYuan - Refactor your thinking
  2. // Copyright (c) 2020-present, b3log.org
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. package model
  17. import (
  18. "bytes"
  19. "fmt"
  20. "path"
  21. "sort"
  22. "strconv"
  23. "strings"
  24. "github.com/88250/gulu"
  25. "github.com/88250/lute"
  26. "github.com/88250/lute/ast"
  27. "github.com/88250/lute/parse"
  28. "github.com/emirpasic/gods/sets/hashset"
  29. "github.com/facette/natsort"
  30. "github.com/siyuan-note/logging"
  31. "github.com/siyuan-note/siyuan/kernel/search"
  32. "github.com/siyuan-note/siyuan/kernel/sql"
  33. "github.com/siyuan-note/siyuan/kernel/treenode"
  34. "github.com/siyuan-note/siyuan/kernel/util"
  35. )
  36. func RefreshBacklink(id string) {
  37. WaitForWritingFiles()
  38. refs := sql.QueryRefsByDefID(id, false)
  39. trees := map[string]*parse.Tree{}
  40. for _, ref := range refs {
  41. tree := trees[ref.RootID]
  42. if nil == tree {
  43. var loadErr error
  44. tree, loadErr = loadTreeByBlockID(ref.RootID)
  45. if nil != loadErr {
  46. logging.LogErrorf("refresh tree refs failed: %s", loadErr)
  47. continue
  48. }
  49. trees[ref.RootID] = tree
  50. sql.UpdateRefsTreeQueue(tree)
  51. }
  52. }
  53. }
  54. type Backlink struct {
  55. DOM string `json:"dom"`
  56. BlockPaths []*BlockPath `json:"blockPaths"`
  57. Expand bool `json:"expand"`
  58. }
  59. func GetBackmentionDoc(defID, refTreeID, keyword string) (ret []*Backlink) {
  60. keyword = strings.TrimSpace(keyword)
  61. ret = []*Backlink{}
  62. beforeLen := 12
  63. sqlBlock := sql.GetBlock(defID)
  64. if nil == sqlBlock {
  65. return
  66. }
  67. rootID := sqlBlock.RootID
  68. refs := sql.QueryRefsByDefID(defID, true)
  69. refs = removeDuplicatedRefs(refs) // 同一个块中引用多个相同块时反链去重 https://github.com/siyuan-note/siyuan/issues/3317
  70. linkRefs, _, excludeBacklinkIDs := buildLinkRefs(rootID, refs, keyword)
  71. tmpMentions, mentionKeywords := buildTreeBackmention(sqlBlock, linkRefs, keyword, excludeBacklinkIDs, beforeLen)
  72. luteEngine := NewLute()
  73. treeCache := map[string]*parse.Tree{}
  74. var mentions []*Block
  75. for _, mention := range tmpMentions {
  76. if mention.RootID == refTreeID {
  77. mentions = append(mentions, mention)
  78. }
  79. }
  80. if "" != keyword {
  81. mentionKeywords = append(mentionKeywords, keyword)
  82. }
  83. mentionKeywords = gulu.Str.RemoveDuplicatedElem(mentionKeywords)
  84. for _, mention := range mentions {
  85. refTree := treeCache[mention.RootID]
  86. if nil == refTree {
  87. var loadErr error
  88. refTree, loadErr = loadTreeByBlockID(mention.ID)
  89. if nil != loadErr {
  90. logging.LogWarnf("load ref tree [%s] failed: %s", mention.ID, loadErr)
  91. continue
  92. }
  93. treeCache[mention.RootID] = refTree
  94. }
  95. backlink := buildBacklink(mention.ID, refTree, mentionKeywords, luteEngine)
  96. ret = append(ret, backlink)
  97. }
  98. return
  99. }
  100. func GetBacklinkDoc(defID, refTreeID, keyword string) (ret []*Backlink) {
  101. keyword = strings.TrimSpace(keyword)
  102. ret = []*Backlink{}
  103. sqlBlock := sql.GetBlock(defID)
  104. if nil == sqlBlock {
  105. return
  106. }
  107. rootID := sqlBlock.RootID
  108. tmpRefs := sql.QueryRefsByDefID(defID, true)
  109. var refs []*sql.Ref
  110. for _, ref := range tmpRefs {
  111. if ref.RootID == refTreeID {
  112. refs = append(refs, ref)
  113. }
  114. }
  115. refs = removeDuplicatedRefs(refs) // 同一个块中引用多个相同块时反链去重 https://github.com/siyuan-note/siyuan/issues/3317
  116. linkRefs, _, _ := buildLinkRefs(rootID, refs, keyword)
  117. refTree, err := loadTreeByBlockID(refTreeID)
  118. if nil != err {
  119. logging.LogWarnf("load ref tree [%s] failed: %s", refTreeID, err)
  120. return
  121. }
  122. luteEngine := NewLute()
  123. for _, linkRef := range linkRefs {
  124. var keywords []string
  125. if "" != keyword {
  126. keywords = append(keywords, keyword)
  127. }
  128. backlink := buildBacklink(linkRef.ID, refTree, keywords, luteEngine)
  129. ret = append(ret, backlink)
  130. }
  131. return
  132. }
  133. func buildBacklink(refID string, refTree *parse.Tree, keywords []string, luteEngine *lute.Lute) (ret *Backlink) {
  134. n := treenode.GetNodeInTree(refTree, refID)
  135. if nil == n {
  136. return
  137. }
  138. renderNodes, expand := getBacklinkRenderNodes(n)
  139. if 0 < len(keywords) {
  140. for _, renderNode := range renderNodes {
  141. var unlinks []*ast.Node
  142. ast.Walk(renderNode, func(n *ast.Node, entering bool) ast.WalkStatus {
  143. if !entering {
  144. return ast.WalkContinue
  145. }
  146. if n.IsBlock() {
  147. return ast.WalkContinue
  148. }
  149. markReplaceSpan(n, &unlinks, keywords, search.MarkDataType, luteEngine)
  150. return ast.WalkContinue
  151. })
  152. for _, unlink := range unlinks {
  153. unlink.Unlink()
  154. }
  155. }
  156. }
  157. dom := renderBlockDOMByNodes(renderNodes, luteEngine)
  158. ret = &Backlink{
  159. DOM: dom,
  160. BlockPaths: buildBlockBreadcrumb(n, nil),
  161. Expand: expand,
  162. }
  163. return
  164. }
  165. func getBacklinkRenderNodes(n *ast.Node) (ret []*ast.Node, expand bool) {
  166. expand = true
  167. if ast.NodeListItem == n.Type {
  168. if nil == n.FirstChild {
  169. return
  170. }
  171. c := n.FirstChild
  172. if 3 == n.ListData.Typ {
  173. c = n.FirstChild.Next
  174. }
  175. if c != n.LastChild { // 存在子列表
  176. for liFirstBlockSpan := c.FirstChild; nil != liFirstBlockSpan; liFirstBlockSpan = liFirstBlockSpan.Next {
  177. if treenode.IsBlockRef(liFirstBlockSpan) {
  178. continue
  179. }
  180. if "" != strings.TrimSpace(liFirstBlockSpan.Text()) {
  181. expand = false
  182. break
  183. }
  184. }
  185. }
  186. ret = append(ret, n)
  187. } else if ast.NodeHeading == n.Type {
  188. c := n.FirstChild
  189. if nil == c {
  190. return
  191. }
  192. for headingFirstSpan := c; nil != headingFirstSpan; headingFirstSpan = headingFirstSpan.Next {
  193. if treenode.IsBlockRef(headingFirstSpan) {
  194. continue
  195. }
  196. if "" != strings.TrimSpace(headingFirstSpan.Text()) {
  197. expand = false
  198. break
  199. }
  200. }
  201. ret = append(ret, n)
  202. cc := treenode.HeadingChildren(n)
  203. ret = append(ret, cc...)
  204. } else {
  205. ret = append(ret, n)
  206. }
  207. return
  208. }
  209. func GetBacklink2(id, keyword, mentionKeyword string, sortMode, mentionSortMode int) (boxID string, backlinks, backmentions []*Path, linkRefsCount, mentionsCount int) {
  210. keyword = strings.TrimSpace(keyword)
  211. mentionKeyword = strings.TrimSpace(mentionKeyword)
  212. backlinks, backmentions = []*Path{}, []*Path{}
  213. sqlBlock := sql.GetBlock(id)
  214. if nil == sqlBlock {
  215. return
  216. }
  217. rootID := sqlBlock.RootID
  218. boxID = sqlBlock.Box
  219. refs := sql.QueryRefsByDefID(id, true)
  220. refs = removeDuplicatedRefs(refs) // 同一个块中引用多个相同块时反链去重 https://github.com/siyuan-note/siyuan/issues/3317
  221. linkRefs, linkRefsCount, excludeBacklinkIDs := buildLinkRefs(rootID, refs, keyword)
  222. tmpBacklinks := toFlatTree(linkRefs, 0, "backlink", nil)
  223. for _, l := range tmpBacklinks {
  224. l.Blocks = nil
  225. backlinks = append(backlinks, l)
  226. }
  227. sort.Slice(backlinks, func(i, j int) bool {
  228. switch sortMode {
  229. case util.SortModeUpdatedDESC:
  230. return backlinks[i].Updated > backlinks[j].Updated
  231. case util.SortModeUpdatedASC:
  232. return backlinks[i].Updated < backlinks[j].Updated
  233. case util.SortModeCreatedDESC:
  234. return backlinks[i].Created > backlinks[j].Created
  235. case util.SortModeCreatedASC:
  236. return backlinks[i].Created < backlinks[j].Created
  237. case util.SortModeNameDESC:
  238. return util.PinYinCompare(util.RemoveEmoji(backlinks[j].Name), util.RemoveEmoji(backlinks[i].Name))
  239. case util.SortModeNameASC:
  240. return util.PinYinCompare(util.RemoveEmoji(backlinks[i].Name), util.RemoveEmoji(backlinks[j].Name))
  241. case util.SortModeAlphanumDESC:
  242. return natsort.Compare(util.RemoveEmoji(backlinks[j].Name), util.RemoveEmoji(backlinks[i].Name))
  243. case util.SortModeAlphanumASC:
  244. return natsort.Compare(util.RemoveEmoji(backlinks[i].Name), util.RemoveEmoji(backlinks[j].Name))
  245. }
  246. return backlinks[i].ID > backlinks[j].ID
  247. })
  248. mentionRefs, _ := buildTreeBackmention(sqlBlock, linkRefs, mentionKeyword, excludeBacklinkIDs, 12)
  249. tmpBackmentions := toFlatTree(mentionRefs, 0, "backlink", nil)
  250. for _, l := range tmpBackmentions {
  251. l.Blocks = nil
  252. backmentions = append(backmentions, l)
  253. }
  254. sort.Slice(backmentions, func(i, j int) bool {
  255. switch mentionSortMode {
  256. case util.SortModeUpdatedDESC:
  257. return backmentions[i].Updated > backmentions[j].Updated
  258. case util.SortModeUpdatedASC:
  259. return backmentions[i].Updated < backmentions[j].Updated
  260. case util.SortModeCreatedDESC:
  261. return backmentions[i].Created > backmentions[j].Created
  262. case util.SortModeCreatedASC:
  263. return backmentions[i].Created < backmentions[j].Created
  264. case util.SortModeNameDESC:
  265. return util.PinYinCompare(util.RemoveEmoji(backmentions[j].Name), util.RemoveEmoji(backmentions[i].Name))
  266. case util.SortModeNameASC:
  267. return util.PinYinCompare(util.RemoveEmoji(backmentions[i].Name), util.RemoveEmoji(backmentions[j].Name))
  268. case util.SortModeAlphanumDESC:
  269. return natsort.Compare(util.RemoveEmoji(backmentions[j].Name), util.RemoveEmoji(backmentions[i].Name))
  270. case util.SortModeAlphanumASC:
  271. return natsort.Compare(util.RemoveEmoji(backmentions[i].Name), util.RemoveEmoji(backmentions[j].Name))
  272. }
  273. return backmentions[i].ID > backmentions[j].ID
  274. })
  275. mentionsCount = len(backmentions)
  276. // 添加笔记本名称
  277. var boxIDs []string
  278. for _, l := range backlinks {
  279. boxIDs = append(boxIDs, l.Box)
  280. }
  281. for _, l := range backmentions {
  282. boxIDs = append(boxIDs, l.Box)
  283. }
  284. boxIDs = gulu.Str.RemoveDuplicatedElem(boxIDs)
  285. boxNames := Conf.BoxNames(boxIDs)
  286. for _, l := range backlinks {
  287. name := boxNames[l.Box]
  288. l.HPath = name + l.HPath
  289. }
  290. for _, l := range backmentions {
  291. name := boxNames[l.Box]
  292. l.HPath = name + l.HPath
  293. }
  294. return
  295. }
  296. func GetBacklink(id, keyword, mentionKeyword string, beforeLen int) (boxID string, linkPaths, mentionPaths []*Path, linkRefsCount, mentionsCount int) {
  297. linkPaths = []*Path{}
  298. mentionPaths = []*Path{}
  299. sqlBlock := sql.GetBlock(id)
  300. if nil == sqlBlock {
  301. return
  302. }
  303. rootID := sqlBlock.RootID
  304. boxID = sqlBlock.Box
  305. var links []*Block
  306. refs := sql.QueryRefsByDefID(id, true)
  307. refs = removeDuplicatedRefs(refs) // 同一个块中引用多个相同块时反链去重 https://github.com/siyuan-note/siyuan/issues/3317
  308. // 为了减少查询,组装好 IDs 后一次查出
  309. defSQLBlockIDs, refSQLBlockIDs := map[string]bool{}, map[string]bool{}
  310. var queryBlockIDs []string
  311. for _, ref := range refs {
  312. defSQLBlockIDs[ref.DefBlockID] = true
  313. refSQLBlockIDs[ref.BlockID] = true
  314. queryBlockIDs = append(queryBlockIDs, ref.DefBlockID)
  315. queryBlockIDs = append(queryBlockIDs, ref.BlockID)
  316. }
  317. querySQLBlocks := sql.GetBlocks(queryBlockIDs)
  318. defSQLBlocksCache := map[string]*sql.Block{}
  319. for _, defSQLBlock := range querySQLBlocks {
  320. if nil != defSQLBlock && defSQLBlockIDs[defSQLBlock.ID] {
  321. defSQLBlocksCache[defSQLBlock.ID] = defSQLBlock
  322. }
  323. }
  324. refSQLBlocksCache := map[string]*sql.Block{}
  325. for _, refSQLBlock := range querySQLBlocks {
  326. if nil != refSQLBlock && refSQLBlockIDs[refSQLBlock.ID] {
  327. refSQLBlocksCache[refSQLBlock.ID] = refSQLBlock
  328. }
  329. }
  330. excludeBacklinkIDs := hashset.New()
  331. for _, ref := range refs {
  332. defSQLBlock := defSQLBlocksCache[(ref.DefBlockID)]
  333. if nil == defSQLBlock {
  334. continue
  335. }
  336. refSQLBlock := refSQLBlocksCache[ref.BlockID]
  337. if nil == refSQLBlock {
  338. continue
  339. }
  340. refBlock := fromSQLBlock(refSQLBlock, "", beforeLen)
  341. if rootID == refBlock.RootID { // 排除当前文档内引用提及
  342. excludeBacklinkIDs.Add(refBlock.RootID, refBlock.ID)
  343. }
  344. defBlock := fromSQLBlock(defSQLBlock, "", beforeLen)
  345. if defBlock.RootID == rootID { // 当前文档的定义块
  346. links = append(links, defBlock)
  347. if ref.DefBlockID == defBlock.ID {
  348. defBlock.Refs = append(defBlock.Refs, refBlock)
  349. }
  350. }
  351. }
  352. for _, link := range links {
  353. for _, ref := range link.Refs {
  354. excludeBacklinkIDs.Add(ref.RootID, ref.ID)
  355. }
  356. linkRefsCount += len(link.Refs)
  357. }
  358. var linkRefs []*Block
  359. processedParagraphs := hashset.New()
  360. var paragraphParentIDs []string
  361. for _, link := range links {
  362. for _, ref := range link.Refs {
  363. if "NodeParagraph" == ref.Type {
  364. paragraphParentIDs = append(paragraphParentIDs, ref.ParentID)
  365. }
  366. }
  367. }
  368. paragraphParents := sql.GetBlocks(paragraphParentIDs)
  369. for _, p := range paragraphParents {
  370. if "i" == p.Type || "h" == p.Type {
  371. linkRefs = append(linkRefs, fromSQLBlock(p, keyword, beforeLen))
  372. processedParagraphs.Add(p.ID)
  373. }
  374. }
  375. for _, link := range links {
  376. for _, ref := range link.Refs {
  377. if "NodeParagraph" == ref.Type {
  378. if processedParagraphs.Contains(ref.ParentID) {
  379. continue
  380. }
  381. }
  382. ref.DefID = link.ID
  383. ref.DefPath = link.Path
  384. content := ref.Content
  385. if "" != keyword {
  386. _, content = search.MarkText(content, keyword, beforeLen, Conf.Search.CaseSensitive)
  387. ref.Content = content
  388. }
  389. linkRefs = append(linkRefs, ref)
  390. }
  391. }
  392. linkPaths = toSubTree(linkRefs, keyword)
  393. mentions, _ := buildTreeBackmention(sqlBlock, linkRefs, mentionKeyword, excludeBacklinkIDs, beforeLen)
  394. mentionsCount = len(mentions)
  395. mentionPaths = toFlatTree(mentions, 0, "backlink", nil)
  396. return
  397. }
  398. func buildLinkRefs(defRootID string, refs []*sql.Ref, keyword string) (ret []*Block, refsCount int, excludeBacklinkIDs *hashset.Set) {
  399. // 为了减少查询,组装好 IDs 后一次查出
  400. defSQLBlockIDs, refSQLBlockIDs := map[string]bool{}, map[string]bool{}
  401. var queryBlockIDs []string
  402. for _, ref := range refs {
  403. defSQLBlockIDs[ref.DefBlockID] = true
  404. refSQLBlockIDs[ref.BlockID] = true
  405. queryBlockIDs = append(queryBlockIDs, ref.DefBlockID)
  406. queryBlockIDs = append(queryBlockIDs, ref.BlockID)
  407. }
  408. queryBlockIDs = gulu.Str.RemoveDuplicatedElem(queryBlockIDs)
  409. querySQLBlocks := sql.GetBlocks(queryBlockIDs)
  410. defSQLBlocksCache := map[string]*sql.Block{}
  411. for _, defSQLBlock := range querySQLBlocks {
  412. if nil != defSQLBlock && defSQLBlockIDs[defSQLBlock.ID] {
  413. defSQLBlocksCache[defSQLBlock.ID] = defSQLBlock
  414. }
  415. }
  416. refSQLBlocksCache := map[string]*sql.Block{}
  417. for _, refSQLBlock := range querySQLBlocks {
  418. if nil != refSQLBlock && refSQLBlockIDs[refSQLBlock.ID] {
  419. refSQLBlocksCache[refSQLBlock.ID] = refSQLBlock
  420. }
  421. }
  422. var links []*Block
  423. excludeBacklinkIDs = hashset.New()
  424. for _, ref := range refs {
  425. defSQLBlock := defSQLBlocksCache[(ref.DefBlockID)]
  426. if nil == defSQLBlock {
  427. continue
  428. }
  429. refSQLBlock := refSQLBlocksCache[ref.BlockID]
  430. if nil == refSQLBlock {
  431. continue
  432. }
  433. refBlock := fromSQLBlock(refSQLBlock, "", 12)
  434. if defRootID == refBlock.RootID { // 排除当前文档内引用提及
  435. excludeBacklinkIDs.Add(refBlock.RootID, refBlock.ID)
  436. }
  437. defBlock := fromSQLBlock(defSQLBlock, "", 12)
  438. if defBlock.RootID == defRootID { // 当前文档的定义块
  439. links = append(links, defBlock)
  440. if ref.DefBlockID == defBlock.ID {
  441. defBlock.Refs = append(defBlock.Refs, refBlock)
  442. }
  443. }
  444. }
  445. for _, link := range links {
  446. for _, ref := range link.Refs {
  447. excludeBacklinkIDs.Add(ref.RootID, ref.ID)
  448. }
  449. refsCount += len(link.Refs)
  450. }
  451. parentRefParagraphs := map[string]*Block{}
  452. for _, link := range links {
  453. for _, ref := range link.Refs {
  454. if "NodeParagraph" == ref.Type {
  455. parentRefParagraphs[ref.ParentID] = ref
  456. }
  457. }
  458. }
  459. var paragraphParentIDs []string
  460. for parentID, _ := range parentRefParagraphs {
  461. paragraphParentIDs = append(paragraphParentIDs, parentID)
  462. }
  463. paragraphParents := sql.GetBlocks(paragraphParentIDs)
  464. processedParagraphs := hashset.New()
  465. for _, p := range paragraphParents {
  466. // 改进标题下方块和列表项子块引用时的反链定位 https://github.com/siyuan-note/siyuan/issues/7484
  467. if "i" == p.Type {
  468. refBlock := parentRefParagraphs[p.ID]
  469. if nil != refBlock && p.FContent == refBlock.Content { // 使用内容判断是否是列表项下第一个子块
  470. // 如果是列表项下第一个子块,则后续会通过列表项传递或关联处理,所以这里就不处理这个段落了
  471. processedParagraphs.Add(p.ID)
  472. if !strings.Contains(p.Content, keyword) {
  473. refsCount--
  474. continue
  475. }
  476. ret = append(ret, fromSQLBlock(p, "", 12))
  477. }
  478. }
  479. }
  480. for _, link := range links {
  481. for _, ref := range link.Refs {
  482. if "NodeParagraph" == ref.Type {
  483. if processedParagraphs.Contains(ref.ParentID) {
  484. continue
  485. }
  486. }
  487. if !strings.Contains(ref.Content, keyword) {
  488. refsCount--
  489. continue
  490. }
  491. ref.DefID = link.ID
  492. ref.DefPath = link.Path
  493. ret = append(ret, ref)
  494. }
  495. }
  496. return
  497. }
  498. func removeDuplicatedRefs(refs []*sql.Ref) (ret []*sql.Ref) {
  499. for _, ref := range refs {
  500. contain := false
  501. for _, r := range ret {
  502. if ref.DefBlockID == r.DefBlockID && ref.BlockID == r.BlockID {
  503. contain = true
  504. break
  505. }
  506. }
  507. if !contain {
  508. ret = append(ret, ref)
  509. }
  510. }
  511. return
  512. }
  513. func buildTreeBackmention(defSQLBlock *sql.Block, refBlocks []*Block, keyword string, excludeBacklinkIDs *hashset.Set, beforeLen int) (ret []*Block, mentionKeywords []string) {
  514. ret = []*Block{}
  515. var names, aliases []string
  516. var fName, rootID string
  517. if "d" == defSQLBlock.Type {
  518. if Conf.Search.BacklinkMentionName {
  519. names = sql.QueryBlockNamesByRootID(defSQLBlock.ID)
  520. }
  521. if Conf.Search.BacklinkMentionAlias {
  522. aliases = sql.QueryBlockAliases(defSQLBlock.ID)
  523. }
  524. if Conf.Search.BacklinkMentionDoc {
  525. fName = path.Base(defSQLBlock.HPath)
  526. }
  527. rootID = defSQLBlock.ID
  528. } else {
  529. if Conf.Search.BacklinkMentionName {
  530. if "" != defSQLBlock.Name {
  531. names = append(names, defSQLBlock.Name)
  532. }
  533. }
  534. if Conf.Search.BacklinkMentionAlias {
  535. if "" != defSQLBlock.Alias {
  536. aliases = strings.Split(defSQLBlock.Alias, ",")
  537. }
  538. }
  539. root := treenode.GetBlockTree(defSQLBlock.RootID)
  540. rootID = root.ID
  541. }
  542. set := hashset.New()
  543. for _, name := range names {
  544. set.Add(name)
  545. }
  546. for _, alias := range aliases {
  547. set.Add(alias)
  548. }
  549. if "" != fName {
  550. set.Add(fName)
  551. }
  552. if Conf.Search.BacklinkMentionAnchor {
  553. for _, refBlock := range refBlocks {
  554. refs := sql.QueryRefsByDefIDRefID(refBlock.DefID, refBlock.ID)
  555. for _, ref := range refs {
  556. set.Add(ref.Content)
  557. }
  558. }
  559. }
  560. for _, v := range set.Values() {
  561. mentionKeywords = append(mentionKeywords, v.(string))
  562. }
  563. mentionKeywords = prepareMarkKeywords(mentionKeywords)
  564. ret = searchBackmention(mentionKeywords, keyword, excludeBacklinkIDs, rootID, beforeLen)
  565. return
  566. }
  567. func searchBackmention(mentionKeywords []string, keyword string, excludeBacklinkIDs *hashset.Set, rootID string, beforeLen int) (ret []*Block) {
  568. ret = []*Block{}
  569. if 1 > len(mentionKeywords) {
  570. return
  571. }
  572. table := "blocks_fts" // 大小写敏感
  573. if !Conf.Search.CaseSensitive {
  574. table = "blocks_fts_case_insensitive"
  575. }
  576. buf := bytes.Buffer{}
  577. buf.WriteString("SELECT * FROM " + table + " WHERE " + table + " MATCH '" + columnFilter() + ":(")
  578. for i, mentionKeyword := range mentionKeywords {
  579. if Conf.Search.BacklinkMentionKeywordsLimit < i {
  580. util.PushMsg(fmt.Sprintf(Conf.Language(38), len(mentionKeywords)), 5000)
  581. mentionKeyword = strings.ReplaceAll(mentionKeyword, "\"", "\"\"")
  582. buf.WriteString("\"" + mentionKeyword + "\"")
  583. break
  584. }
  585. mentionKeyword = strings.ReplaceAll(mentionKeyword, "\"", "\"\"")
  586. buf.WriteString("\"" + mentionKeyword + "\"")
  587. if i < len(mentionKeywords)-1 {
  588. buf.WriteString(" OR ")
  589. }
  590. }
  591. buf.WriteString(")")
  592. if "" != keyword {
  593. keyword = strings.ReplaceAll(keyword, "\"", "\"\"")
  594. buf.WriteString(" AND (\"" + keyword + "\")")
  595. }
  596. buf.WriteString("'")
  597. buf.WriteString(" AND root_id != '" + rootID + "'") // 不在定义块所在文档中搜索
  598. buf.WriteString(" AND type IN ('d', 'h', 'p', 't')")
  599. buf.WriteString(" ORDER BY id DESC LIMIT " + strconv.Itoa(Conf.Search.Limit))
  600. query := buf.String()
  601. sqlBlocks := sql.SelectBlocksRawStmt(query, 1, Conf.Search.Limit)
  602. terms := mentionKeywords
  603. if "" != keyword {
  604. terms = append(terms, keyword)
  605. }
  606. blocks := fromSQLBlocks(&sqlBlocks, strings.Join(terms, search.TermSep), beforeLen)
  607. luteEngine := util.NewLute()
  608. var tmp []*Block
  609. for _, b := range blocks {
  610. tree := parse.Parse("", gulu.Str.ToBytes(b.Markdown), luteEngine.ParseOptions)
  611. if nil == tree {
  612. continue
  613. }
  614. textBuf := &bytes.Buffer{}
  615. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  616. if !entering || n.IsBlock() {
  617. return ast.WalkContinue
  618. }
  619. if ast.NodeText == n.Type { // 这里包含了标签命中的情况,因为 Lute 没有启用 TextMark
  620. textBuf.Write(n.Tokens)
  621. }
  622. return ast.WalkContinue
  623. })
  624. text := textBuf.String()
  625. text = strings.TrimSpace(text)
  626. if "" == text {
  627. continue
  628. }
  629. newText := markReplaceSpanWithSplit(text, mentionKeywords, search.GetMarkSpanStart(search.MarkDataType), search.GetMarkSpanEnd())
  630. if text != newText {
  631. tmp = append(tmp, b)
  632. } else {
  633. // columnFilter 中的命名、别名和备注命中的情况
  634. // 反链提及搜索范围增加命名、别名和备注 https://github.com/siyuan-note/siyuan/issues/7639
  635. if gulu.Str.Contains(trimMarkTags(b.Name), mentionKeywords) ||
  636. gulu.Str.Contains(trimMarkTags(b.Alias), mentionKeywords) ||
  637. gulu.Str.Contains(trimMarkTags(b.Memo), mentionKeywords) {
  638. tmp = append(tmp, b)
  639. }
  640. }
  641. }
  642. blocks = tmp
  643. mentionBlockMap := map[string]*Block{}
  644. for _, block := range blocks {
  645. mentionBlockMap[block.ID] = block
  646. refText := getContainStr(block.Content, mentionKeywords)
  647. block.RefText = refText
  648. }
  649. for _, mentionBlock := range mentionBlockMap {
  650. if !excludeBacklinkIDs.Contains(mentionBlock.ID) {
  651. ret = append(ret, mentionBlock)
  652. }
  653. }
  654. sort.SliceStable(ret, func(i, j int) bool {
  655. return ret[i].ID > ret[j].ID
  656. })
  657. return
  658. }
  659. func trimMarkTags(str string) string {
  660. return strings.TrimSuffix(strings.TrimPrefix(str, "<mark>"), "</mark>")
  661. }
  662. func getContainStr(str string, strs []string) string {
  663. str = strings.ToLower(str)
  664. for _, s := range strs {
  665. if strings.Contains(str, strings.ToLower(s)) {
  666. return s
  667. }
  668. }
  669. return ""
  670. }
  671. // buildFullLinks 构建正向和反向链接列表。
  672. // forwardlinks:正向链接关系 refs
  673. // backlinks:反向链接关系 defs
  674. func buildFullLinks(condition string) (forwardlinks, backlinks []*Block) {
  675. forwardlinks, backlinks = []*Block{}, []*Block{}
  676. defs := buildDefsAndRefs(condition)
  677. backlinks = append(backlinks, defs...)
  678. for _, def := range defs {
  679. for _, ref := range def.Refs {
  680. forwardlinks = append(forwardlinks, ref)
  681. }
  682. }
  683. return
  684. }
  685. func buildDefsAndRefs(condition string) (defBlocks []*Block) {
  686. defBlockMap := map[string]*Block{}
  687. refBlockMap := map[string]*Block{}
  688. defRefs := sql.DefRefs(condition)
  689. // 将 sql block 转为 block
  690. for _, row := range defRefs {
  691. for def, ref := range row {
  692. if nil == ref {
  693. continue
  694. }
  695. refBlock := refBlockMap[ref.ID]
  696. if nil == refBlock {
  697. refBlock = fromSQLBlock(ref, "", 0)
  698. refBlockMap[ref.ID] = refBlock
  699. }
  700. // ref 块自己也需要作为定义块,否则图上没有节点
  701. if defBlock := defBlockMap[ref.ID]; nil == defBlock {
  702. defBlockMap[ref.ID] = refBlock
  703. }
  704. if defBlock := defBlockMap[def.ID]; nil == defBlock {
  705. defBlock = fromSQLBlock(def, "", 0)
  706. defBlockMap[def.ID] = defBlock
  707. }
  708. }
  709. }
  710. // 组装 block.Defs 和 block.Refs 字段
  711. for _, row := range defRefs {
  712. for def, ref := range row {
  713. if nil == ref {
  714. defBlock := fromSQLBlock(def, "", 0)
  715. defBlockMap[def.ID] = defBlock
  716. continue
  717. }
  718. refBlock := refBlockMap[ref.ID]
  719. defBlock := defBlockMap[def.ID]
  720. if refBlock.ID == defBlock.ID { // 自引用
  721. continue
  722. }
  723. refBlock.Defs = append(refBlock.Defs, defBlock)
  724. defBlock.Refs = append(defBlock.Refs, refBlock)
  725. }
  726. }
  727. for _, def := range defBlockMap {
  728. defBlocks = append(defBlocks, def)
  729. }
  730. return
  731. }