search.go 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143
  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. "errors"
  20. "fmt"
  21. "math"
  22. "os"
  23. "path"
  24. "path/filepath"
  25. "regexp"
  26. "sort"
  27. "strconv"
  28. "strings"
  29. "time"
  30. "unicode/utf8"
  31. "github.com/88250/gulu"
  32. "github.com/88250/lute"
  33. "github.com/88250/lute/ast"
  34. "github.com/88250/lute/lex"
  35. "github.com/88250/lute/parse"
  36. "github.com/88250/vitess-sqlparser/sqlparser"
  37. "github.com/jinzhu/copier"
  38. "github.com/siyuan-note/filelock"
  39. "github.com/siyuan-note/logging"
  40. "github.com/siyuan-note/siyuan/kernel/conf"
  41. "github.com/siyuan-note/siyuan/kernel/search"
  42. "github.com/siyuan-note/siyuan/kernel/sql"
  43. "github.com/siyuan-note/siyuan/kernel/task"
  44. "github.com/siyuan-note/siyuan/kernel/treenode"
  45. "github.com/siyuan-note/siyuan/kernel/util"
  46. "github.com/xrash/smetrics"
  47. )
  48. type EmbedBlock struct {
  49. Block *Block `json:"block"`
  50. BlockPaths []*BlockPath `json:"blockPaths"`
  51. }
  52. func SearchEmbedBlock(embedBlockID, stmt string, excludeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) {
  53. return searchEmbedBlock(embedBlockID, stmt, excludeIDs, headingMode, breadcrumb)
  54. }
  55. func searchEmbedBlock(embedBlockID, stmt string, excludeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) {
  56. sqlBlocks := sql.SelectBlocksRawStmtNoParse(stmt, Conf.Search.Limit)
  57. var tmp []*sql.Block
  58. for _, b := range sqlBlocks {
  59. if "query_embed" == b.Type { // 嵌入块不再嵌入
  60. // 嵌入块支持搜索 https://github.com/siyuan-note/siyuan/issues/7112
  61. // 这里会导致上面的 limit 限制不准确,导致结果变少,暂时没有解决方案,只能靠用户自己调整 SQL,加上 type != 'query_embed' 的条件
  62. continue
  63. }
  64. if !gulu.Str.Contains(b.ID, excludeIDs) {
  65. tmp = append(tmp, b)
  66. }
  67. }
  68. sqlBlocks = tmp
  69. // 缓存最多 128 棵语法树
  70. trees := map[string]*parse.Tree{}
  71. count := 0
  72. for _, sb := range sqlBlocks {
  73. if nil == trees[sb.RootID] {
  74. tree, _ := loadTreeByBlockID(sb.RootID)
  75. if nil == tree {
  76. continue
  77. }
  78. trees[sb.RootID] = tree
  79. count++
  80. }
  81. if 127 < count {
  82. break
  83. }
  84. }
  85. for _, sb := range sqlBlocks {
  86. block, blockPaths := getEmbeddedBlock(embedBlockID, trees, sb, headingMode, breadcrumb)
  87. if nil == block {
  88. continue
  89. }
  90. ret = append(ret, &EmbedBlock{
  91. Block: block,
  92. BlockPaths: blockPaths,
  93. })
  94. }
  95. // 嵌入块支持搜索 https://github.com/siyuan-note/siyuan/issues/7112
  96. task.AppendTaskWithTimeout(task.DatabaseIndexEmbedBlock, 30*time.Second, updateEmbedBlockContent, embedBlockID, ret)
  97. // 添加笔记本名称
  98. var boxIDs []string
  99. for _, embedBlock := range ret {
  100. boxIDs = append(boxIDs, embedBlock.Block.Box)
  101. }
  102. boxIDs = gulu.Str.RemoveDuplicatedElem(boxIDs)
  103. boxNames := Conf.BoxNames(boxIDs)
  104. for _, embedBlock := range ret {
  105. name := boxNames[embedBlock.Block.Box]
  106. embedBlock.Block.HPath = name + embedBlock.Block.HPath
  107. }
  108. if 1 > len(ret) {
  109. ret = []*EmbedBlock{}
  110. }
  111. return
  112. }
  113. func SearchRefBlock(id, rootID, keyword string, beforeLen int, isSquareBrackets bool) (ret []*Block, newDoc bool) {
  114. cachedTrees := map[string]*parse.Tree{}
  115. onlyDoc := false
  116. if isSquareBrackets {
  117. onlyDoc = Conf.Editor.OnlySearchForDoc
  118. }
  119. if "" == keyword {
  120. // 查询为空时默认的块引排序规则按最近使用优先 https://github.com/siyuan-note/siyuan/issues/3218
  121. refs := sql.QueryRefsRecent(onlyDoc)
  122. for _, ref := range refs {
  123. tree := cachedTrees[ref.DefBlockRootID]
  124. if nil == tree {
  125. tree, _ = loadTreeByBlockID(ref.DefBlockRootID)
  126. }
  127. if nil == tree {
  128. continue
  129. }
  130. cachedTrees[ref.RootID] = tree
  131. node := treenode.GetNodeInTree(tree, ref.DefBlockID)
  132. if nil == node {
  133. continue
  134. }
  135. sqlBlock := sql.BuildBlockFromNode(node, tree)
  136. if nil == sqlBlock {
  137. return
  138. }
  139. block := fromSQLBlock(sqlBlock, "", 0)
  140. block.RefText = getNodeRefText(node)
  141. block.RefText = maxContent(block.RefText, Conf.Editor.BlockRefDynamicAnchorTextMaxLen)
  142. ret = append(ret, block)
  143. }
  144. if 1 > len(ret) {
  145. ret = []*Block{}
  146. }
  147. return
  148. }
  149. ret = fullTextSearchRefBlock(keyword, beforeLen, onlyDoc)
  150. tmp := ret[:0]
  151. for _, b := range ret {
  152. tree := cachedTrees[b.RootID]
  153. if nil == tree {
  154. tree, _ = loadTreeByBlockID(b.RootID)
  155. }
  156. if nil == tree {
  157. continue
  158. }
  159. cachedTrees[b.RootID] = tree
  160. b.RefText = getBlockRefText(b.ID, tree)
  161. hitFirstChildID := false
  162. if b.IsContainerBlock() {
  163. // `((` 引用候选中排除当前块的父块 https://github.com/siyuan-note/siyuan/issues/4538
  164. tree := cachedTrees[b.RootID]
  165. if nil == tree {
  166. tree, _ = loadTreeByBlockID(b.RootID)
  167. cachedTrees[b.RootID] = tree
  168. }
  169. if nil != tree {
  170. bNode := treenode.GetNodeInTree(tree, b.ID)
  171. if fc := treenode.FirstLeafBlock(bNode); nil != fc && fc.ID == id {
  172. hitFirstChildID = true
  173. }
  174. }
  175. }
  176. if b.ID != id && !hitFirstChildID && b.ID != rootID {
  177. tmp = append(tmp, b)
  178. }
  179. }
  180. ret = tmp
  181. if "" != keyword {
  182. if block := treenode.GetBlockTree(id); nil != block {
  183. p := path.Join(block.HPath, keyword)
  184. newDoc = nil == treenode.GetBlockTreeRootByHPath(block.BoxID, p)
  185. }
  186. }
  187. return
  188. }
  189. func FindReplace(keyword, replacement string, ids []string, paths, boxes []string, types map[string]bool, method, orderBy, groupBy int) (err error) {
  190. // method:0:文本,1:查询语法,2:SQL,3:正则表达式
  191. if 1 == method || 2 == method {
  192. err = errors.New(Conf.Language(132))
  193. return
  194. }
  195. keyword = strings.TrimSpace(keyword)
  196. replacement = strings.TrimSpace(replacement)
  197. if keyword == replacement {
  198. return
  199. }
  200. r, _ := regexp.Compile(keyword)
  201. escapedKey := util.EscapeHTML(keyword)
  202. escapedR, _ := regexp.Compile(escapedKey)
  203. ids = gulu.Str.RemoveDuplicatedElem(ids)
  204. var renameRoots []*ast.Node
  205. renameRootTitles := map[string]string{}
  206. cachedTrees := map[string]*parse.Tree{}
  207. historyDir, err := getHistoryDir(HistoryOpReplace, time.Now())
  208. if nil != err {
  209. logging.LogErrorf("get history dir failed: %s", err)
  210. return
  211. }
  212. if 1 > len(ids) {
  213. // `Replace All` is no longer affected by pagination https://github.com/siyuan-note/siyuan/issues/8265
  214. blocks, _, _, _ := FullTextSearchBlock(keyword, boxes, paths, types, method, orderBy, groupBy, 1, math.MaxInt)
  215. for _, block := range blocks {
  216. ids = append(ids, block.ID)
  217. }
  218. }
  219. for _, id := range ids {
  220. bt := treenode.GetBlockTree(id)
  221. if nil == bt {
  222. continue
  223. }
  224. tree := cachedTrees[bt.RootID]
  225. if nil != tree {
  226. continue
  227. }
  228. tree, _ = loadTreeByBlockID(id)
  229. if nil == tree {
  230. continue
  231. }
  232. historyPath := filepath.Join(historyDir, tree.Box, tree.Path)
  233. if err = os.MkdirAll(filepath.Dir(historyPath), 0755); nil != err {
  234. logging.LogErrorf("generate history failed: %s", err)
  235. return
  236. }
  237. var data []byte
  238. if data, err = filelock.ReadFile(filepath.Join(util.DataDir, tree.Box, tree.Path)); err != nil {
  239. logging.LogErrorf("generate history failed: %s", err)
  240. return
  241. }
  242. if err = gulu.File.WriteFileSafer(historyPath, data, 0644); err != nil {
  243. logging.LogErrorf("generate history failed: %s", err)
  244. return
  245. }
  246. cachedTrees[bt.RootID] = tree
  247. }
  248. indexHistoryDir(filepath.Base(historyDir), util.NewLute())
  249. for i, id := range ids {
  250. bt := treenode.GetBlockTree(id)
  251. if nil == bt {
  252. continue
  253. }
  254. tree := cachedTrees[bt.RootID]
  255. if nil == tree {
  256. continue
  257. }
  258. node := treenode.GetNodeInTree(tree, id)
  259. if nil == node {
  260. continue
  261. }
  262. if ast.NodeDocument == node.Type {
  263. title := node.IALAttr("title")
  264. if 0 == method {
  265. if strings.Contains(title, keyword) {
  266. renameRootTitles[node.ID] = strings.ReplaceAll(title, keyword, replacement)
  267. renameRoots = append(renameRoots, node)
  268. }
  269. } else if 3 == method {
  270. if nil != r && r.MatchString(title) {
  271. renameRootTitles[node.ID] = r.ReplaceAllString(title, replacement)
  272. renameRoots = append(renameRoots, node)
  273. }
  274. }
  275. } else {
  276. ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
  277. if !entering {
  278. return ast.WalkContinue
  279. }
  280. switch n.Type {
  281. case ast.NodeText, ast.NodeLinkDest, ast.NodeLinkText, ast.NodeLinkTitle, ast.NodeCodeBlockCode, ast.NodeMathBlockContent:
  282. if 0 == method {
  283. if bytes.Contains(n.Tokens, []byte(keyword)) {
  284. n.Tokens = bytes.ReplaceAll(n.Tokens, []byte(keyword), []byte(replacement))
  285. }
  286. } else if 3 == method {
  287. if nil != r && r.MatchString(string(n.Tokens)) {
  288. n.Tokens = []byte(r.ReplaceAllString(string(n.Tokens), replacement))
  289. }
  290. }
  291. case ast.NodeTextMark:
  292. if n.IsTextMarkType("code") {
  293. if 0 == method {
  294. if strings.Contains(n.TextMarkTextContent, escapedKey) {
  295. n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, escapedKey, replacement)
  296. }
  297. } else if 3 == method {
  298. if nil != escapedR && escapedR.MatchString(n.TextMarkTextContent) {
  299. n.TextMarkTextContent = escapedR.ReplaceAllString(n.TextMarkTextContent, replacement)
  300. }
  301. }
  302. } else {
  303. if 0 == method {
  304. if bytes.Contains(n.Tokens, []byte(keyword)) {
  305. n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, keyword, replacement)
  306. }
  307. } else if 3 == method {
  308. if nil != r && r.MatchString(n.TextMarkTextContent) {
  309. n.TextMarkTextContent = r.ReplaceAllString(n.TextMarkTextContent, replacement)
  310. }
  311. }
  312. }
  313. if 0 == method {
  314. if strings.Contains(n.TextMarkTextContent, keyword) {
  315. n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, keyword, replacement)
  316. }
  317. if strings.Contains(n.TextMarkInlineMathContent, keyword) {
  318. n.TextMarkInlineMathContent = strings.ReplaceAll(n.TextMarkInlineMathContent, keyword, replacement)
  319. }
  320. if strings.Contains(n.TextMarkInlineMemoContent, keyword) {
  321. n.TextMarkInlineMemoContent = strings.ReplaceAll(n.TextMarkInlineMemoContent, keyword, replacement)
  322. }
  323. if strings.Contains(n.TextMarkATitle, keyword) {
  324. n.TextMarkATitle = strings.ReplaceAll(n.TextMarkATitle, keyword, replacement)
  325. }
  326. if strings.Contains(n.TextMarkAHref, keyword) {
  327. n.TextMarkAHref = strings.ReplaceAll(n.TextMarkAHref, keyword, replacement)
  328. }
  329. } else if 3 == method {
  330. if nil != r {
  331. if r.MatchString(n.TextMarkTextContent) {
  332. n.TextMarkTextContent = r.ReplaceAllString(n.TextMarkTextContent, replacement)
  333. }
  334. if r.MatchString(n.TextMarkInlineMathContent) {
  335. n.TextMarkInlineMathContent = r.ReplaceAllString(n.TextMarkInlineMathContent, replacement)
  336. }
  337. if r.MatchString(n.TextMarkInlineMemoContent) {
  338. n.TextMarkInlineMemoContent = r.ReplaceAllString(n.TextMarkInlineMemoContent, replacement)
  339. }
  340. if r.MatchString(n.TextMarkATitle) {
  341. n.TextMarkATitle = r.ReplaceAllString(n.TextMarkATitle, replacement)
  342. }
  343. if r.MatchString(n.TextMarkAHref) {
  344. n.TextMarkAHref = r.ReplaceAllString(n.TextMarkAHref, replacement)
  345. }
  346. }
  347. }
  348. }
  349. return ast.WalkContinue
  350. })
  351. if err = writeJSONQueue(tree); nil != err {
  352. return
  353. }
  354. }
  355. util.PushEndlessProgress(fmt.Sprintf(Conf.Language(206), i+1, len(ids)))
  356. }
  357. for i, renameRoot := range renameRoots {
  358. newTitle := renameRootTitles[renameRoot.ID]
  359. RenameDoc(renameRoot.Box, renameRoot.Path, newTitle)
  360. util.PushEndlessProgress(fmt.Sprintf(Conf.Language(207), i+1, len(renameRoots)))
  361. }
  362. WaitForWritingFiles()
  363. if 0 < len(ids) {
  364. go func() {
  365. time.Sleep(time.Millisecond * 500)
  366. util.ReloadUI()
  367. }()
  368. }
  369. return
  370. }
  371. // FullTextSearchBlock 搜索内容块。
  372. //
  373. // method:0:关键字,1:查询语法,2:SQL,3:正则表达式
  374. // orderBy: 0:按块类型(默认),1:按创建时间升序,2:按创建时间降序,3:按更新时间升序,4:按更新时间降序,5:按内容顺序(仅在按文档分组时),6:按相关度升序,7:按相关度降序
  375. // groupBy:0:不分组,1:按文档分组
  376. func FullTextSearchBlock(query string, boxes, paths []string, types map[string]bool, method, orderBy, groupBy, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount, pageCount int) {
  377. query = strings.TrimSpace(query)
  378. beforeLen := 36
  379. var blocks []*Block
  380. orderByClause := buildOrderBy(method, orderBy)
  381. switch method {
  382. case 1: // 查询语法
  383. filter := buildTypeFilter(types)
  384. boxFilter := buildBoxesFilter(boxes)
  385. pathFilter := buildPathsFilter(paths)
  386. blocks, matchedBlockCount, matchedRootCount = fullTextSearchByQuerySyntax(query, boxFilter, pathFilter, filter, orderByClause, beforeLen, page, pageSize)
  387. case 2: // SQL
  388. blocks, matchedBlockCount, matchedRootCount = searchBySQL(query, beforeLen, page, pageSize)
  389. case 3: // 正则表达式
  390. typeFilter := buildTypeFilter(types)
  391. boxFilter := buildBoxesFilter(boxes)
  392. pathFilter := buildPathsFilter(paths)
  393. blocks, matchedBlockCount, matchedRootCount = fullTextSearchByRegexp(query, boxFilter, pathFilter, typeFilter, orderByClause, beforeLen, page, pageSize)
  394. default: // 关键字
  395. filter := buildTypeFilter(types)
  396. boxFilter := buildBoxesFilter(boxes)
  397. pathFilter := buildPathsFilter(paths)
  398. blocks, matchedBlockCount, matchedRootCount = fullTextSearchByKeyword(query, boxFilter, pathFilter, filter, orderByClause, beforeLen, page, pageSize)
  399. }
  400. pageCount = (matchedBlockCount + pageSize - 1) / pageSize
  401. switch groupBy {
  402. case 0: // 不分组
  403. ret = blocks
  404. case 1: // 按文档分组
  405. rootMap := map[string]bool{}
  406. var rootIDs []string
  407. contentSorts := map[string]int{}
  408. for _, b := range blocks {
  409. if _, ok := rootMap[b.RootID]; !ok {
  410. rootMap[b.RootID] = true
  411. rootIDs = append(rootIDs, b.RootID)
  412. tree, _ := loadTreeByBlockID(b.RootID)
  413. if nil == tree {
  414. continue
  415. }
  416. if 5 == orderBy { // 按内容顺序(仅在按文档分组时)
  417. sort := 0
  418. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  419. if !entering || !n.IsBlock() {
  420. return ast.WalkContinue
  421. }
  422. contentSorts[n.ID] = sort
  423. sort++
  424. return ast.WalkContinue
  425. })
  426. }
  427. }
  428. }
  429. sqlRoots := sql.GetBlocks(rootIDs)
  430. roots := fromSQLBlocks(&sqlRoots, "", beforeLen)
  431. for _, root := range roots {
  432. for _, b := range blocks {
  433. if 5 == orderBy { // 按内容顺序(仅在按文档分组时)
  434. b.Sort = contentSorts[b.ID]
  435. }
  436. if b.RootID == root.ID {
  437. root.Children = append(root.Children, b)
  438. }
  439. }
  440. switch orderBy {
  441. case 1: //按创建时间升序
  442. sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Created < root.Children[j].Created })
  443. case 2: // 按创建时间降序
  444. sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Created > root.Children[j].Created })
  445. case 3: // 按更新时间升序
  446. sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Updated < root.Children[j].Updated })
  447. case 4: // 按更新时间降序
  448. sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Updated > root.Children[j].Updated })
  449. case 5: // 按内容顺序(仅在按文档分组时)
  450. sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Sort < root.Children[j].Sort })
  451. default: // 按块类型(默认)
  452. sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Sort < root.Children[j].Sort })
  453. }
  454. }
  455. switch orderBy {
  456. case 1: //按创建时间升序
  457. sort.Slice(roots, func(i, j int) bool { return roots[i].Created < roots[j].Created })
  458. case 2: // 按创建时间降序
  459. sort.Slice(roots, func(i, j int) bool { return roots[i].Created > roots[j].Created })
  460. case 3: // 按更新时间升序
  461. sort.Slice(roots, func(i, j int) bool { return roots[i].Updated < roots[j].Updated })
  462. case 4: // 按更新时间降序
  463. sort.Slice(roots, func(i, j int) bool { return roots[i].Updated > roots[j].Updated })
  464. case 5: // 按内容顺序(仅在按文档分组时)
  465. // 都是文档,不需要再次排序
  466. case 6, 7: // 按相关度
  467. // 已在 ORDER BY 中处理
  468. default: // 按块类型(默认)
  469. // 都是文档,不需要再次排序
  470. }
  471. ret = roots
  472. default:
  473. ret = blocks
  474. }
  475. if 1 > len(ret) {
  476. ret = []*Block{}
  477. }
  478. return
  479. }
  480. func buildBoxesFilter(boxes []string) string {
  481. if 0 == len(boxes) {
  482. return ""
  483. }
  484. builder := bytes.Buffer{}
  485. builder.WriteString(" AND (")
  486. for i, box := range boxes {
  487. builder.WriteString(fmt.Sprintf("box = '%s'", box))
  488. if i < len(boxes)-1 {
  489. builder.WriteString(" OR ")
  490. }
  491. }
  492. builder.WriteString(")")
  493. return builder.String()
  494. }
  495. func buildPathsFilter(paths []string) string {
  496. if 0 == len(paths) {
  497. return ""
  498. }
  499. builder := bytes.Buffer{}
  500. builder.WriteString(" AND (")
  501. for i, path := range paths {
  502. builder.WriteString(fmt.Sprintf("path LIKE '%s%%'", path))
  503. if i < len(paths)-1 {
  504. builder.WriteString(" OR ")
  505. }
  506. }
  507. builder.WriteString(")")
  508. return builder.String()
  509. }
  510. func buildOrderBy(method, orderBy int) string {
  511. switch orderBy {
  512. case 1:
  513. return "ORDER BY created ASC"
  514. case 2:
  515. return "ORDER BY created DESC"
  516. case 3:
  517. return "ORDER BY updated ASC"
  518. case 4:
  519. return "ORDER BY updated DESC"
  520. case 6:
  521. if 0 != method && 1 != method {
  522. // 只有关键字搜索和查询语法搜索才支持按相关度升序 https://github.com/siyuan-note/siyuan/issues/7861
  523. return "ORDER BY sort DESC, updated DESC"
  524. }
  525. return "ORDER BY rank DESC" // 默认是按相关度降序,所以按相关度升序要反过来使用 DESC
  526. case 7:
  527. if 0 != method && 1 != method {
  528. return "ORDER BY sort ASC, updated DESC"
  529. }
  530. return "ORDER BY rank" // 默认是按相关度降序
  531. default:
  532. return "ORDER BY sort ASC, updated DESC" // Improve search default sort https://github.com/siyuan-note/siyuan/issues/8624
  533. }
  534. }
  535. func buildTypeFilter(types map[string]bool) string {
  536. s := conf.NewSearch()
  537. if err := copier.Copy(s, Conf.Search); nil != err {
  538. logging.LogErrorf("copy search conf failed: %s", err)
  539. }
  540. if nil != types {
  541. s.Document = types["document"]
  542. s.Heading = types["heading"]
  543. s.List = types["list"]
  544. s.ListItem = types["listItem"]
  545. s.CodeBlock = types["codeBlock"]
  546. s.MathBlock = types["mathBlock"]
  547. s.Table = types["table"]
  548. s.Blockquote = types["blockquote"]
  549. s.SuperBlock = types["superBlock"]
  550. s.Paragraph = types["paragraph"]
  551. s.HTMLBlock = types["htmlBlock"]
  552. s.EmbedBlock = types["embedBlock"]
  553. } else {
  554. s.Document = Conf.Search.Document
  555. s.Heading = Conf.Search.Heading
  556. s.List = Conf.Search.List
  557. s.ListItem = Conf.Search.ListItem
  558. s.CodeBlock = Conf.Search.CodeBlock
  559. s.MathBlock = Conf.Search.MathBlock
  560. s.Table = Conf.Search.Table
  561. s.Blockquote = Conf.Search.Blockquote
  562. s.SuperBlock = Conf.Search.SuperBlock
  563. s.Paragraph = Conf.Search.Paragraph
  564. s.HTMLBlock = Conf.Search.HTMLBlock
  565. s.EmbedBlock = Conf.Search.EmbedBlock
  566. }
  567. return s.TypeFilter()
  568. }
  569. func searchBySQL(stmt string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
  570. stmt = gulu.Str.RemoveInvisible(stmt)
  571. stmt = strings.TrimSpace(stmt)
  572. blocks := sql.SelectBlocksRawStmt(stmt, page, pageSize)
  573. ret = fromSQLBlocks(&blocks, "", beforeLen)
  574. if 1 > len(ret) {
  575. ret = []*Block{}
  576. return
  577. }
  578. stmt = strings.ToLower(stmt)
  579. if strings.HasPrefix(stmt, "select a.* ") { // 多个搜索关键字匹配文档 https://github.com/siyuan-note/siyuan/issues/7350
  580. stmt = strings.ReplaceAll(stmt, "select a.* ", "select COUNT(a.id) AS `matches`, COUNT(DISTINCT(a.root_id)) AS `docs` ")
  581. } else {
  582. stmt = strings.ReplaceAll(stmt, "select * ", "select COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` ")
  583. }
  584. stmt = removeLimitClause(stmt)
  585. result, _ := sql.QueryNoLimit(stmt)
  586. if 1 > len(ret) {
  587. return
  588. }
  589. matchedBlockCount = int(result[0]["matches"].(int64))
  590. matchedRootCount = int(result[0]["docs"].(int64))
  591. return
  592. }
  593. func removeLimitClause(stmt string) string {
  594. parsedStmt, err := sqlparser.Parse(stmt)
  595. if nil != err {
  596. return stmt
  597. }
  598. switch parsedStmt.(type) {
  599. case *sqlparser.Select:
  600. slct := parsedStmt.(*sqlparser.Select)
  601. if nil != slct.Limit {
  602. slct.Limit = nil
  603. }
  604. stmt = sqlparser.String(slct)
  605. }
  606. return stmt
  607. }
  608. func fullTextSearchRefBlock(keyword string, beforeLen int, onlyDoc bool) (ret []*Block) {
  609. keyword = gulu.Str.RemoveInvisible(keyword)
  610. if ast.IsNodeIDPattern(keyword) {
  611. ret, _, _ = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+keyword+"'", 36, 1, 32)
  612. return
  613. }
  614. quotedKeyword := stringQuery(keyword)
  615. table := "blocks_fts" // 大小写敏感
  616. if !Conf.Search.CaseSensitive {
  617. table = "blocks_fts_case_insensitive"
  618. }
  619. projections := "id, parent_id, root_id, hash, box, path, " +
  620. "snippet(" + table + ", 6, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS hpath, " +
  621. "snippet(" + table + ", 7, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS name, " +
  622. "snippet(" + table + ", 8, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS alias, " +
  623. "snippet(" + table + ", 9, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS memo, " +
  624. "tag, " +
  625. "snippet(" + table + ", 11, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS content, " +
  626. "fcontent, markdown, length, type, subtype, ial, sort, created, updated"
  627. stmt := "SELECT " + projections + " FROM " + table + " WHERE " + table + " MATCH '" + columnFilter() + ":(" + quotedKeyword + ")' AND type"
  628. if onlyDoc {
  629. stmt += " = 'd'"
  630. } else {
  631. stmt += " IN " + Conf.Search.TypeFilter()
  632. }
  633. orderBy := ` order by case
  634. when name = '${keyword}' then 10
  635. when alias = '${keyword}' then 20
  636. when memo = '${keyword}' then 30
  637. when content = '${keyword}' and type = 'd' then 40
  638. when content LIKE '%${keyword}%' and type = 'd' then 41
  639. when name LIKE '%${keyword}%' then 50
  640. when alias LIKE '%${keyword}%' then 60
  641. when content = '${keyword}' and type = 'h' then 70
  642. when content LIKE '%${keyword}%' and type = 'h' then 71
  643. when fcontent = '${keyword}' and type = 'i' then 80
  644. when fcontent LIKE '%${keyword}%' and type = 'i' then 81
  645. when memo LIKE '%${keyword}%' then 90
  646. when content LIKE '%${keyword}%' and type != 'i' and type != 'l' then 100
  647. else 65535 end ASC, sort ASC, length ASC`
  648. orderBy = strings.ReplaceAll(orderBy, "${keyword}", quotedKeyword)
  649. stmt += orderBy + " LIMIT " + strconv.Itoa(Conf.Search.Limit)
  650. blocks := sql.SelectBlocksRawStmtNoParse(stmt, Conf.Search.Limit)
  651. ret = fromSQLBlocks(&blocks, "", beforeLen)
  652. if 1 > len(ret) {
  653. ret = []*Block{}
  654. }
  655. return
  656. }
  657. func fullTextSearchByQuerySyntax(query, boxFilter, pathFilter, typeFilter, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
  658. query = gulu.Str.RemoveInvisible(query)
  659. if ast.IsNodeIDPattern(query) {
  660. ret, matchedBlockCount, matchedRootCount = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+query+"'", beforeLen, page, pageSize)
  661. return
  662. }
  663. return fullTextSearchByFTS(query, boxFilter, pathFilter, typeFilter, orderBy, beforeLen, page, pageSize)
  664. }
  665. func fullTextSearchByKeyword(query, boxFilter, pathFilter, typeFilter string, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
  666. query = gulu.Str.RemoveInvisible(query)
  667. if ast.IsNodeIDPattern(query) {
  668. ret, matchedBlockCount, matchedRootCount = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+query+"'", beforeLen, page, pageSize)
  669. return
  670. }
  671. query = stringQuery(query)
  672. return fullTextSearchByFTS(query, boxFilter, pathFilter, typeFilter, orderBy, beforeLen, page, pageSize)
  673. }
  674. func fullTextSearchByRegexp(exp, boxFilter, pathFilter, typeFilter, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
  675. exp = gulu.Str.RemoveInvisible(exp)
  676. fieldFilter := fieldRegexp(exp)
  677. stmt := "SELECT * FROM `blocks` WHERE " + fieldFilter + " AND type IN " + typeFilter
  678. stmt += boxFilter + pathFilter
  679. stmt += " " + orderBy
  680. stmt += " LIMIT " + strconv.Itoa(pageSize) + " OFFSET " + strconv.Itoa((page-1)*pageSize)
  681. blocks := sql.SelectBlocksRawStmtNoParse(stmt, Conf.Search.Limit)
  682. ret = fromSQLBlocks(&blocks, "", beforeLen)
  683. if 1 > len(ret) {
  684. ret = []*Block{}
  685. }
  686. matchedBlockCount, matchedRootCount = fullTextSearchCountByRegexp(exp, boxFilter, pathFilter, typeFilter)
  687. return
  688. }
  689. func fullTextSearchCountByRegexp(exp, boxFilter, pathFilter, typeFilter string) (matchedBlockCount, matchedRootCount int) {
  690. fieldFilter := fieldRegexp(exp)
  691. stmt := "SELECT COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` FROM `blocks` WHERE " + fieldFilter + " AND type IN " + typeFilter
  692. stmt += boxFilter + pathFilter
  693. result, _ := sql.QueryNoLimit(stmt)
  694. if 1 > len(result) {
  695. return
  696. }
  697. matchedBlockCount = int(result[0]["matches"].(int64))
  698. matchedRootCount = int(result[0]["docs"].(int64))
  699. return
  700. }
  701. func fullTextSearchByFTS(query, boxFilter, pathFilter, typeFilter, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
  702. table := "blocks_fts" // 大小写敏感
  703. if !Conf.Search.CaseSensitive {
  704. table = "blocks_fts_case_insensitive"
  705. }
  706. projections := "id, parent_id, root_id, hash, box, path, " +
  707. "highlight(" + table + ", 6, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS hpath, " +
  708. "highlight(" + table + ", 7, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS name, " +
  709. "highlight(" + table + ", 8, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS alias, " +
  710. "highlight(" + table + ", 9, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS memo, " +
  711. "tag, " +
  712. "highlight(" + table + ", 11, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS content, " +
  713. "fcontent, markdown, length, type, subtype, ial, sort, created, updated"
  714. stmt := "SELECT " + projections + " FROM " + table + " WHERE (`" + table + "` MATCH '" + columnFilter() + ":(" + query + ")'"
  715. stmt += ") AND type IN " + typeFilter
  716. stmt += boxFilter + pathFilter
  717. stmt += " " + orderBy
  718. stmt += " LIMIT " + strconv.Itoa(pageSize) + " OFFSET " + strconv.Itoa((page-1)*pageSize)
  719. blocks := sql.SelectBlocksRawStmt(stmt, page, pageSize)
  720. ret = fromSQLBlocks(&blocks, "", beforeLen)
  721. if 1 > len(ret) {
  722. ret = []*Block{}
  723. }
  724. matchedBlockCount, matchedRootCount = fullTextSearchCount(query, boxFilter, pathFilter, typeFilter)
  725. return
  726. }
  727. func highlightByQuery(query, typeFilter, id string) (ret []string) {
  728. const limit = 256
  729. table := "blocks_fts"
  730. if !Conf.Search.CaseSensitive {
  731. table = "blocks_fts_case_insensitive"
  732. }
  733. projections := "id, parent_id, root_id, hash, box, path, " +
  734. "highlight(" + table + ", 6, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS hpath, " +
  735. "highlight(" + table + ", 7, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS name, " +
  736. "highlight(" + table + ", 8, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS alias, " +
  737. "highlight(" + table + ", 9, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS memo, " +
  738. "tag, " +
  739. "highlight(" + table + ", 11, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS content, " +
  740. "fcontent, markdown, length, type, subtype, ial, sort, created, updated"
  741. stmt := "SELECT " + projections + " FROM " + table + " WHERE (`" + table + "` MATCH '" + columnFilter() + ":(" + query + ")'"
  742. stmt += ") AND type IN " + typeFilter
  743. stmt += " AND root_id = '" + id + "'"
  744. stmt += " LIMIT " + strconv.Itoa(limit)
  745. sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, limit)
  746. for _, block := range sqlBlocks {
  747. keyword := gulu.Str.SubstringsBetween(block.Content, search.SearchMarkLeft, search.SearchMarkRight)
  748. if 0 < len(keyword) {
  749. ret = append(ret, keyword...)
  750. }
  751. }
  752. ret = gulu.Str.RemoveDuplicatedElem(ret)
  753. return
  754. }
  755. func fullTextSearchCount(query, boxFilter, pathFilter, typeFilter string) (matchedBlockCount, matchedRootCount int) {
  756. query = gulu.Str.RemoveInvisible(query)
  757. if ast.IsNodeIDPattern(query) {
  758. ret, _ := sql.QueryNoLimit("SELECT COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` FROM `blocks` WHERE `id` = '" + query + "'")
  759. if 1 > len(ret) {
  760. return
  761. }
  762. matchedBlockCount = int(ret[0]["matches"].(int64))
  763. matchedRootCount = int(ret[0]["docs"].(int64))
  764. return
  765. }
  766. table := "blocks_fts" // 大小写敏感
  767. if !Conf.Search.CaseSensitive {
  768. table = "blocks_fts_case_insensitive"
  769. }
  770. stmt := "SELECT COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` FROM `" + table + "` WHERE (`" + table + "` MATCH '" + columnFilter() + ":(" + query + ")'"
  771. stmt += ") AND type IN " + typeFilter
  772. stmt += boxFilter + pathFilter
  773. result, _ := sql.QueryNoLimit(stmt)
  774. if 1 > len(result) {
  775. return
  776. }
  777. matchedBlockCount = int(result[0]["matches"].(int64))
  778. matchedRootCount = int(result[0]["docs"].(int64))
  779. return
  780. }
  781. func markSearch(text string, keyword string, beforeLen int) (marked string, score float64) {
  782. if 0 == len(keyword) {
  783. marked = text
  784. if strings.Contains(marked, search.SearchMarkLeft) { // 使用 FTS snippet() 处理过高亮片段,这里简单替换后就返回
  785. marked = util.EscapeHTML(text)
  786. marked = strings.ReplaceAll(marked, search.SearchMarkLeft, "<mark>")
  787. marked = strings.ReplaceAll(marked, search.SearchMarkRight, "</mark>")
  788. return
  789. }
  790. keywords := gulu.Str.SubstringsBetween(marked, search.SearchMarkLeft, search.SearchMarkRight)
  791. keywords = gulu.Str.RemoveDuplicatedElem(keywords)
  792. keyword = strings.Join(keywords, search.TermSep)
  793. marked = strings.ReplaceAll(marked, search.SearchMarkLeft, "")
  794. marked = strings.ReplaceAll(marked, search.SearchMarkRight, "")
  795. _, marked = search.MarkText(marked, keyword, beforeLen, Conf.Search.CaseSensitive)
  796. return
  797. }
  798. pos, marked := search.MarkText(text, keyword, beforeLen, Conf.Search.CaseSensitive)
  799. if -1 < pos {
  800. if 0 == pos {
  801. score = 1
  802. }
  803. score += float64(strings.Count(marked, "<mark>"))
  804. winkler := smetrics.JaroWinkler(text, keyword, 0.7, 4)
  805. score += winkler
  806. }
  807. score = -score // 分越小排序越靠前
  808. return
  809. }
  810. func fromSQLBlocks(sqlBlocks *[]*sql.Block, terms string, beforeLen int) (ret []*Block) {
  811. for _, sqlBlock := range *sqlBlocks {
  812. ret = append(ret, fromSQLBlock(sqlBlock, terms, beforeLen))
  813. }
  814. return
  815. }
  816. func fromSQLBlock(sqlBlock *sql.Block, terms string, beforeLen int) (block *Block) {
  817. if nil == sqlBlock {
  818. return
  819. }
  820. id := sqlBlock.ID
  821. content := util.EscapeHTML(sqlBlock.Content) // Search dialog XSS https://github.com/siyuan-note/siyuan/issues/8525
  822. content, _ = markSearch(content, terms, beforeLen)
  823. content = maxContent(content, 5120)
  824. markdown := maxContent(sqlBlock.Markdown, 5120)
  825. block = &Block{
  826. Box: sqlBlock.Box,
  827. Path: sqlBlock.Path,
  828. ID: id,
  829. RootID: sqlBlock.RootID,
  830. ParentID: sqlBlock.ParentID,
  831. Alias: sqlBlock.Alias,
  832. Name: sqlBlock.Name,
  833. Memo: sqlBlock.Memo,
  834. Tag: sqlBlock.Tag,
  835. Content: content,
  836. FContent: sqlBlock.FContent,
  837. Markdown: markdown,
  838. Type: treenode.FromAbbrType(sqlBlock.Type),
  839. SubType: sqlBlock.SubType,
  840. Sort: sqlBlock.Sort,
  841. }
  842. if "" != sqlBlock.IAL {
  843. block.IAL = map[string]string{}
  844. ialStr := strings.TrimPrefix(sqlBlock.IAL, "{:")
  845. ialStr = strings.TrimSuffix(ialStr, "}")
  846. ial := parse.Tokens2IAL([]byte(ialStr))
  847. for _, kv := range ial {
  848. block.IAL[kv[0]] = kv[1]
  849. }
  850. }
  851. hPath, _ := markSearch(sqlBlock.HPath, terms, 18)
  852. if !strings.HasPrefix(hPath, "/") {
  853. hPath = "/" + hPath
  854. }
  855. block.HPath = hPath
  856. if "" != block.Name {
  857. block.Name, _ = markSearch(block.Name, terms, 256)
  858. }
  859. if "" != block.Alias {
  860. block.Alias, _ = markSearch(block.Alias, terms, 256)
  861. }
  862. if "" != block.Memo {
  863. block.Memo, _ = markSearch(block.Memo, terms, 256)
  864. }
  865. return
  866. }
  867. func maxContent(content string, maxLen int) string {
  868. idx := strings.Index(content, "<mark>")
  869. if 128 < maxLen && maxLen <= idx {
  870. head := bytes.Buffer{}
  871. for i := 0; i < 512; i++ {
  872. r, size := utf8.DecodeLastRuneInString(content[:idx])
  873. head.WriteRune(r)
  874. idx -= size
  875. if 64 < head.Len() {
  876. break
  877. }
  878. }
  879. content = util.Reverse(head.String()) + content[idx:]
  880. }
  881. if maxLen < utf8.RuneCountInString(content) {
  882. return gulu.Str.SubStr(content, maxLen) + "..."
  883. }
  884. return content
  885. }
  886. func fieldRegexp(regexp string) string {
  887. buf := bytes.Buffer{}
  888. buf.WriteString("(")
  889. buf.WriteString("content REGEXP '")
  890. buf.WriteString(regexp)
  891. buf.WriteString("'")
  892. if Conf.Search.Name {
  893. buf.WriteString(" OR name REGEXP '")
  894. buf.WriteString(regexp)
  895. buf.WriteString("'")
  896. }
  897. if Conf.Search.Alias {
  898. buf.WriteString(" OR alias REGEXP '")
  899. buf.WriteString(regexp)
  900. buf.WriteString("'")
  901. }
  902. if Conf.Search.Memo {
  903. buf.WriteString(" OR memo REGEXP '")
  904. buf.WriteString(regexp)
  905. buf.WriteString("'")
  906. }
  907. if Conf.Search.IAL {
  908. buf.WriteString(" OR ial REGEXP '")
  909. buf.WriteString(regexp)
  910. buf.WriteString("'")
  911. }
  912. buf.WriteString(" OR tag REGEXP '")
  913. buf.WriteString(regexp)
  914. buf.WriteString("')")
  915. return buf.String()
  916. }
  917. func columnFilter() string {
  918. buf := bytes.Buffer{}
  919. buf.WriteString("{content")
  920. if Conf.Search.Name {
  921. buf.WriteString(" name")
  922. }
  923. if Conf.Search.Alias {
  924. buf.WriteString(" alias")
  925. }
  926. if Conf.Search.Memo {
  927. buf.WriteString(" memo")
  928. }
  929. if Conf.Search.IAL {
  930. buf.WriteString(" ial")
  931. }
  932. buf.WriteString(" tag}")
  933. return buf.String()
  934. }
  935. func stringQuery(query string) string {
  936. query = strings.ReplaceAll(query, "\"", "\"\"")
  937. query = strings.ReplaceAll(query, "'", "''")
  938. buf := bytes.Buffer{}
  939. parts := strings.Split(query, " ")
  940. for _, part := range parts {
  941. part = strings.TrimSpace(part)
  942. part = "\"" + part + "\""
  943. buf.WriteString(part)
  944. buf.WriteString(" ")
  945. }
  946. return strings.TrimSpace(buf.String())
  947. }
  948. // markReplaceSpan 用于处理搜索高亮。
  949. func markReplaceSpan(n *ast.Node, unlinks *[]*ast.Node, keywords []string, markSpanDataType string, luteEngine *lute.Lute) bool {
  950. text := n.Content()
  951. if ast.NodeText == n.Type {
  952. text = search.EncloseHighlighting(text, keywords, search.GetMarkSpanStart(markSpanDataType), search.GetMarkSpanEnd(), Conf.Search.CaseSensitive, false)
  953. n.Tokens = gulu.Str.ToBytes(text)
  954. if bytes.Contains(n.Tokens, []byte(search.MarkDataType)) {
  955. linkTree := parse.Inline("", n.Tokens, luteEngine.ParseOptions)
  956. var children []*ast.Node
  957. for c := linkTree.Root.FirstChild.FirstChild; nil != c; c = c.Next {
  958. children = append(children, c)
  959. }
  960. for _, c := range children {
  961. n.InsertBefore(c)
  962. }
  963. *unlinks = append(*unlinks, n)
  964. return true
  965. }
  966. } else if ast.NodeTextMark == n.Type {
  967. // 搜索结果高亮支持大部分行级元素 https://github.com/siyuan-note/siyuan/issues/6745
  968. if n.IsTextMarkType("inline-math") || n.IsTextMarkType("inline-memo") {
  969. return false
  970. }
  971. startTag := search.GetMarkSpanStart(markSpanDataType)
  972. text = search.EncloseHighlighting(text, keywords, startTag, search.GetMarkSpanEnd(), Conf.Search.CaseSensitive, false)
  973. if strings.Contains(text, search.MarkDataType) {
  974. dataType := search.GetMarkSpanStart(n.TextMarkType + " " + search.MarkDataType)
  975. text = strings.ReplaceAll(text, startTag, dataType)
  976. tokens := gulu.Str.ToBytes(text)
  977. linkTree := parse.Inline("", tokens, luteEngine.ParseOptions)
  978. var children []*ast.Node
  979. for c := linkTree.Root.FirstChild.FirstChild; nil != c; c = c.Next {
  980. if ast.NodeText == c.Type {
  981. c.Type = ast.NodeTextMark
  982. c.TextMarkType = n.TextMarkType
  983. c.TextMarkTextContent = string(c.Tokens)
  984. if n.IsTextMarkType("a") {
  985. c.TextMarkAHref, c.TextMarkATitle = n.TextMarkAHref, n.TextMarkATitle
  986. } else if treenode.IsBlockRef(n) {
  987. c.TextMarkBlockRefID = n.TextMarkBlockRefID
  988. c.TextMarkBlockRefSubtype = n.TextMarkBlockRefSubtype
  989. } else if treenode.IsFileAnnotationRef(n) {
  990. c.TextMarkFileAnnotationRefID = n.TextMarkFileAnnotationRefID
  991. }
  992. } else if ast.NodeTextMark == c.Type {
  993. if n.IsTextMarkType("a") {
  994. c.TextMarkAHref, c.TextMarkATitle = n.TextMarkAHref, n.TextMarkATitle
  995. } else if treenode.IsBlockRef(n) {
  996. c.TextMarkBlockRefID = n.TextMarkBlockRefID
  997. c.TextMarkBlockRefSubtype = n.TextMarkBlockRefSubtype
  998. } else if treenode.IsFileAnnotationRef(n) {
  999. c.TextMarkFileAnnotationRefID = n.TextMarkFileAnnotationRefID
  1000. }
  1001. }
  1002. children = append(children, c)
  1003. if nil != n.Next && ast.NodeKramdownSpanIAL == n.Next.Type {
  1004. c.KramdownIAL = n.KramdownIAL
  1005. ial := &ast.Node{Type: ast.NodeKramdownSpanIAL, Tokens: n.Next.Tokens}
  1006. children = append(children, ial)
  1007. }
  1008. }
  1009. for _, c := range children {
  1010. n.InsertBefore(c)
  1011. }
  1012. *unlinks = append(*unlinks, n)
  1013. return true
  1014. }
  1015. }
  1016. return false
  1017. }
  1018. // markReplaceSpanWithSplit 用于处理虚拟引用和反链提及高亮。
  1019. func markReplaceSpanWithSplit(text string, keywords []string, replacementStart, replacementEnd string) (ret string) {
  1020. // 虚拟引用和反链提及关键字按最长匹配优先 https://github.com/siyuan-note/siyuan/issues/7465
  1021. sort.Slice(keywords, func(i, j int) bool { return len(keywords[i]) > len(keywords[j]) })
  1022. tmp := search.EncloseHighlighting(text, keywords, replacementStart, replacementEnd, Conf.Search.CaseSensitive, true)
  1023. parts := strings.Split(tmp, replacementEnd)
  1024. buf := bytes.Buffer{}
  1025. for i := 0; i < len(parts); i++ {
  1026. if i >= len(parts)-1 {
  1027. buf.WriteString(parts[i])
  1028. break
  1029. }
  1030. if nextPart := parts[i+1]; 0 < len(nextPart) && lex.IsASCIILetter(nextPart[0]) {
  1031. // 取消已经高亮的部分
  1032. part := strings.ReplaceAll(parts[i], replacementStart, "")
  1033. buf.WriteString(part)
  1034. continue
  1035. }
  1036. buf.WriteString(parts[i])
  1037. buf.WriteString(replacementEnd)
  1038. }
  1039. ret = buf.String()
  1040. return
  1041. }