backlink.go 24 KB

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