A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 2287 lines 77 kB view raw
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 17package model 18 19import ( 20 "bytes" 21 "errors" 22 "fmt" 23 "math" 24 "os" 25 "path" 26 "path/filepath" 27 "regexp" 28 "sort" 29 "strconv" 30 "strings" 31 "sync" 32 "time" 33 "unicode/utf8" 34 35 "github.com/88250/gulu" 36 "github.com/88250/lute" 37 "github.com/88250/lute/ast" 38 "github.com/88250/lute/editor" 39 "github.com/88250/lute/html" 40 "github.com/88250/lute/lex" 41 "github.com/88250/lute/parse" 42 "github.com/88250/vitess-sqlparser/sqlparser" 43 "github.com/jinzhu/copier" 44 "github.com/siyuan-note/filelock" 45 "github.com/siyuan-note/logging" 46 "github.com/siyuan-note/siyuan/kernel/conf" 47 "github.com/siyuan-note/siyuan/kernel/search" 48 "github.com/siyuan-note/siyuan/kernel/sql" 49 "github.com/siyuan-note/siyuan/kernel/task" 50 "github.com/siyuan-note/siyuan/kernel/treenode" 51 "github.com/siyuan-note/siyuan/kernel/util" 52 "github.com/xrash/smetrics" 53) 54 55func ListInvalidBlockRefs(page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount, pageCount int) { 56 refBlockMap := map[string][]string{} 57 blockMap := map[string]bool{} 58 var invalidBlockIDs []string 59 notebooks, err := ListNotebooks() 60 if err != nil { 61 return 62 } 63 luteEngine := util.NewLute() 64 for _, notebook := range notebooks { 65 pages := pagedPaths(filepath.Join(util.DataDir, notebook.ID), 32) 66 for _, paths := range pages { 67 var trees []*parse.Tree 68 for _, localPath := range paths { 69 tree, loadTreeErr := loadTree(localPath, luteEngine) 70 if nil != loadTreeErr { 71 continue 72 } 73 trees = append(trees, tree) 74 } 75 for _, tree := range trees { 76 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 77 if entering { 78 if n.IsBlock() { 79 blockMap[n.ID] = true 80 return ast.WalkContinue 81 } 82 83 if ast.NodeTextMark == n.Type { 84 if n.IsTextMarkType("a") { 85 if strings.HasPrefix(n.TextMarkAHref, "siyuan://blocks/") { 86 defID := strings.TrimPrefix(n.TextMarkAHref, "siyuan://blocks/") 87 if strings.Contains(defID, "?") { 88 defID = strings.Split(defID, "?")[0] 89 } 90 refID := treenode.ParentBlock(n).ID 91 if defIDs := refBlockMap[refID]; 1 > len(defIDs) { 92 refBlockMap[refID] = []string{defID} 93 } else { 94 refBlockMap[refID] = append(defIDs, defID) 95 } 96 } 97 } else if n.IsTextMarkType("block-ref") { 98 defID := n.TextMarkBlockRefID 99 refID := treenode.ParentBlock(n).ID 100 if defIDs := refBlockMap[refID]; 1 > len(defIDs) { 101 refBlockMap[refID] = []string{defID} 102 } else { 103 refBlockMap[refID] = append(defIDs, defID) 104 } 105 } 106 } 107 } 108 return ast.WalkContinue 109 }) 110 } 111 } 112 } 113 114 invalidDefIDs := map[string]bool{} 115 for _, refDefIDs := range refBlockMap { 116 for _, defID := range refDefIDs { 117 invalidDefIDs[defID] = true 118 } 119 } 120 121 var toRemoves []string 122 for defID := range invalidDefIDs { 123 if _, ok := blockMap[defID]; ok { 124 toRemoves = append(toRemoves, defID) 125 } 126 } 127 for _, toRemove := range toRemoves { 128 delete(invalidDefIDs, toRemove) 129 } 130 131 toRemoves = nil 132 for refID, defIDs := range refBlockMap { 133 var tmp []string 134 for _, defID := range defIDs { 135 if _, ok := invalidDefIDs[defID]; !ok { 136 tmp = append(tmp, defID) 137 } 138 } 139 140 for _, toRemove := range tmp { 141 defIDs = gulu.Str.RemoveElem(defIDs, toRemove) 142 } 143 144 if 1 > len(defIDs) { 145 toRemoves = append(toRemoves, refID) 146 } 147 } 148 for _, toRemove := range toRemoves { 149 delete(refBlockMap, toRemove) 150 } 151 152 for refID := range refBlockMap { 153 invalidBlockIDs = append(invalidBlockIDs, refID) 154 } 155 invalidBlockIDs = gulu.Str.RemoveDuplicatedElem(invalidBlockIDs) 156 157 sort.Strings(invalidBlockIDs) 158 allInvalidBlockIDs := invalidBlockIDs 159 160 start := (page - 1) * pageSize 161 end := page * pageSize 162 if end > len(invalidBlockIDs) { 163 end = len(invalidBlockIDs) 164 } 165 invalidBlockIDs = invalidBlockIDs[start:end] 166 167 sqlBlocks := sql.GetBlocks(invalidBlockIDs) 168 var tmp []*sql.Block 169 for _, sqlBlock := range sqlBlocks { 170 if nil != sqlBlock { 171 tmp = append(tmp, sqlBlock) 172 } 173 } 174 sqlBlocks = tmp 175 176 ret = fromSQLBlocks(&sqlBlocks, "", 36) 177 if 1 > len(ret) { 178 ret = []*Block{} 179 } 180 matchedBlockCount = len(allInvalidBlockIDs) 181 rootCount := map[string]bool{} 182 for _, id := range allInvalidBlockIDs { 183 bt := treenode.GetBlockTree(id) 184 if nil == bt { 185 continue 186 } 187 rootCount[bt.RootID] = true 188 } 189 matchedRootCount = len(rootCount) 190 pageCount = (matchedBlockCount + pageSize - 1) / pageSize 191 return 192} 193 194type EmbedBlock struct { 195 Block *Block `json:"block"` 196 BlockPaths []*BlockPath `json:"blockPaths"` 197} 198 199func UpdateEmbedBlock(id, content string) (err error) { 200 bt := treenode.GetBlockTree(id) 201 if nil == bt { 202 err = ErrBlockNotFound 203 return 204 } 205 206 if treenode.TypeAbbr(ast.NodeBlockQueryEmbed.String()) != bt.Type { 207 err = errors.New("not query embed block") 208 return 209 } 210 211 embedBlock := &EmbedBlock{ 212 Block: &Block{ 213 Markdown: content, 214 }, 215 } 216 217 updateEmbedBlockContent(id, []*EmbedBlock{embedBlock}) 218 return 219} 220 221func GetEmbedBlock(embedBlockID string, includeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) { 222 return getEmbedBlock(embedBlockID, includeIDs, headingMode, breadcrumb) 223} 224 225func getEmbedBlock(embedBlockID string, includeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) { 226 stmt := "SELECT * FROM `blocks` WHERE `id` IN ('" + strings.Join(includeIDs, "','") + "')" 227 sqlBlocks := sql.SelectBlocksRawStmtNoParse(stmt, 1024) 228 229 // 根据 includeIDs 的顺序排序 Improve `//!js` query embed block result sorting https://github.com/siyuan-note/siyuan/issues/9977 230 m := map[string]int{} 231 for i, id := range includeIDs { 232 m[id] = i 233 } 234 sort.Slice(sqlBlocks, func(i, j int) bool { 235 return m[sqlBlocks[i].ID] < m[sqlBlocks[j].ID] 236 }) 237 238 ret = buildEmbedBlock(embedBlockID, []string{}, headingMode, breadcrumb, sqlBlocks) 239 return 240} 241 242func SearchEmbedBlock(embedBlockID, stmt string, excludeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) { 243 return searchEmbedBlock(embedBlockID, stmt, excludeIDs, headingMode, breadcrumb) 244} 245 246func searchEmbedBlock(embedBlockID, stmt string, excludeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) { 247 sqlBlocks := sql.SelectBlocksRawStmtNoParse(stmt, Conf.Search.Limit) 248 ret = buildEmbedBlock(embedBlockID, excludeIDs, headingMode, breadcrumb, sqlBlocks) 249 return 250} 251 252func buildEmbedBlock(embedBlockID string, excludeIDs []string, headingMode int, breadcrumb bool, sqlBlocks []*sql.Block) (ret []*EmbedBlock) { 253 var tmp []*sql.Block 254 for _, b := range sqlBlocks { 255 if "query_embed" == b.Type { // 嵌入块不再嵌入 256 // 嵌入块支持搜索 https://github.com/siyuan-note/siyuan/issues/7112 257 // 这里会导致上面的 limit 限制不准确,导致结果变少,暂时没有解决方案,只能靠用户自己调整 SQL,加上 type != 'query_embed' 的条件 258 continue 259 } 260 if !gulu.Str.Contains(b.ID, excludeIDs) { 261 tmp = append(tmp, b) 262 } 263 } 264 sqlBlocks = tmp 265 266 // 缓存最多 128 棵语法树 267 trees := map[string]*parse.Tree{} 268 count := 0 269 for _, sb := range sqlBlocks { 270 if nil == trees[sb.RootID] { 271 tree, _ := LoadTreeByBlockID(sb.RootID) 272 if nil == tree { 273 continue 274 } 275 trees[sb.RootID] = tree 276 count++ 277 } 278 if 127 < count { 279 break 280 } 281 } 282 283 for _, sb := range sqlBlocks { 284 block, blockPaths := getEmbeddedBlock(trees, sb, headingMode, breadcrumb) 285 if nil == block { 286 continue 287 } 288 ret = append(ret, &EmbedBlock{ 289 Block: block, 290 BlockPaths: blockPaths, 291 }) 292 } 293 294 // 嵌入块支持搜索 https://github.com/siyuan-note/siyuan/issues/7112 295 task.AppendTaskWithTimeout(task.DatabaseIndexEmbedBlock, 30*time.Second, updateEmbedBlockContent, embedBlockID, ret) 296 297 // 添加笔记本名称 298 var boxIDs []string 299 for _, embedBlock := range ret { 300 boxIDs = append(boxIDs, embedBlock.Block.Box) 301 } 302 boxIDs = gulu.Str.RemoveDuplicatedElem(boxIDs) 303 boxNames := Conf.BoxNames(boxIDs) 304 for _, embedBlock := range ret { 305 name := boxNames[embedBlock.Block.Box] 306 embedBlock.Block.HPath = name + embedBlock.Block.HPath 307 } 308 309 if 1 > len(ret) { 310 ret = []*EmbedBlock{} 311 } 312 return 313} 314 315func SearchRefBlock(id, rootID, keyword string, beforeLen int, isSquareBrackets, isDatabase bool) (ret []*Block, newDoc bool) { 316 cachedTrees := map[string]*parse.Tree{} 317 nodeTrees := map[string]*parse.Tree{} 318 var nodeIDs []string 319 var nodes []*ast.Node 320 321 onlyDoc := false 322 if isSquareBrackets { 323 onlyDoc = Conf.Editor.OnlySearchForDoc 324 } 325 326 if "" == keyword { 327 // 查询为空时默认的块引排序规则按最近使用优先 https://github.com/siyuan-note/siyuan/issues/3218 328 329 typeFilter := Conf.Search.TypeFilter() 330 ignoreLines := getRefSearchIgnoreLines() 331 refs := sql.QueryRefsRecent(onlyDoc, typeFilter, ignoreLines) 332 var btsID []string 333 for _, ref := range refs { 334 btsID = append(btsID, ref.DefBlockRootID) 335 } 336 btsID = gulu.Str.RemoveDuplicatedElem(btsID) 337 bts := treenode.GetBlockTrees(btsID) 338 339 for _, ref := range refs { 340 tree := cachedTrees[ref.DefBlockRootID] 341 if nil == tree { 342 tree, _ = loadTreeByBlockTree(bts[ref.DefBlockRootID]) 343 } 344 if nil == tree { 345 continue 346 } 347 cachedTrees[ref.RootID] = tree 348 349 node := treenode.GetNodeInTree(tree, ref.DefBlockID) 350 if nil == node { 351 continue 352 } 353 354 nodes = append(nodes, node) 355 nodeIDs = append(nodeIDs, node.ID) 356 nodeTrees[node.ID] = tree 357 } 358 359 refCount := sql.QueryRefCount(nodeIDs) 360 361 for _, node := range nodes { 362 tree := nodeTrees[node.ID] 363 sqlBlock := sql.BuildBlockFromNode(node, tree) 364 if nil == sqlBlock { 365 return 366 } 367 368 block := fromSQLBlock(sqlBlock, "", 0) 369 block.RefText = getNodeRefText(node) 370 block.RefText = maxContent(block.RefText, Conf.Editor.BlockRefDynamicAnchorTextMaxLen) 371 block.RefCount = refCount[node.ID] 372 ret = append(ret, block) 373 } 374 375 if 1 > len(ret) { 376 ret = []*Block{} 377 } 378 379 prependNotebookNameInHPath(ret) 380 filterSelfHPath(ret) 381 return 382 } 383 384 ret = fullTextSearchRefBlock(keyword, beforeLen, onlyDoc) 385 tmp := ret[:0] 386 var btsID []string 387 for _, b := range ret { 388 btsID = append(btsID, b.RootID) 389 } 390 btsID = gulu.Str.RemoveDuplicatedElem(btsID) 391 bts := treenode.GetBlockTrees(btsID) 392 for _, b := range ret { 393 tree := cachedTrees[b.RootID] 394 if nil == tree { 395 tree, _ = loadTreeByBlockTree(bts[b.RootID]) 396 } 397 if nil == tree { 398 continue 399 } 400 cachedTrees[b.RootID] = tree 401 b.RefText = getBlockRefText(b.ID, tree) 402 403 hitFirstChildID := false 404 if b.IsContainerBlock() && "NodeDocument" != b.Type { 405 // `((` 引用候选中排除当前块的父块 https://github.com/siyuan-note/siyuan/issues/4538 406 tree = cachedTrees[b.RootID] 407 if nil == tree { 408 tree, _ = loadTreeByBlockTree(bts[b.RootID]) 409 cachedTrees[b.RootID] = tree 410 } 411 if nil != tree { 412 bNode := treenode.GetNodeInTree(tree, b.ID) 413 if fc := treenode.FirstLeafBlock(bNode); nil != fc && fc.ID == id { 414 hitFirstChildID = true 415 } 416 } 417 } 418 419 if "NodeAttributeView" == b.Type { 420 // 数据库块可以添加到自身数据库块中,当前文档也可以添加到自身数据库块中 421 tmp = append(tmp, b) 422 nodeIDs = append(nodeIDs, b.ID) 423 nodeTrees[b.ID] = tree 424 } else { 425 // 排除自身块、父块和根块 426 if b.ID != id && !hitFirstChildID && b.ID != rootID { 427 tmp = append(tmp, b) 428 nodeIDs = append(nodeIDs, b.ID) 429 nodeTrees[b.ID] = tree 430 } 431 } 432 433 } 434 ret = tmp 435 436 refCount := sql.QueryRefCount(nodeIDs) 437 for _, b := range ret { 438 b.RefCount = refCount[b.ID] 439 } 440 441 if !isDatabase { 442 // 如果非数据库中搜索块引,则不允许新建重名文档 443 if block := treenode.GetBlockTree(id); nil != block { 444 p := path.Join(block.HPath, keyword) 445 newDoc = nil == treenode.GetBlockTreeRootByHPath(block.BoxID, p) 446 } 447 } else { // 如果是数据库中搜索绑定块,则允许新建重名文档 https://github.com/siyuan-note/siyuan/issues/11713 448 newDoc = true 449 } 450 451 prependNotebookNameInHPath(ret) 452 filterSelfHPath(ret) 453 return 454} 455 456func filterSelfHPath(blocks []*Block) { 457 // 简化搜索结果列表中的文档块路径 Simplify document block paths in search results https://github.com/siyuan-note/siyuan/issues/13364 458 // 文档块不显示自己的路径(最后一层) 459 460 for _, b := range blocks { 461 if b.IsDoc() { 462 b.HPath = strings.TrimSuffix(b.HPath, path.Base(b.HPath)) 463 } 464 } 465} 466 467func prependNotebookNameInHPath(blocks []*Block) { 468 // 在 hPath 中加入笔记本名 Show notebooks in hpath of block ref search list results https://github.com/siyuan-note/siyuan/issues/9378 469 470 var boxIDs []string 471 for _, b := range blocks { 472 boxIDs = append(boxIDs, b.Box) 473 } 474 boxIDs = gulu.Str.RemoveDuplicatedElem(boxIDs) 475 boxNames := Conf.BoxNames(boxIDs) 476 for _, b := range blocks { 477 name := boxNames[b.Box] 478 b.HPath = util.EscapeHTML(name) + b.HPath 479 } 480} 481 482func FindReplace(keyword, replacement string, replaceTypes map[string]bool, ids []string, paths, boxes []string, types map[string]bool, method, orderBy, groupBy int) (err error) { 483 // method:0:文本,1:查询语法,2:SQL,3:正则表达式 484 if 2 == method { 485 err = errors.New(Conf.Language(132)) 486 return 487 } 488 489 if 1 == method { 490 // 将查询语法等价于关键字,因为 keyword 参数已经是结果关键字了 491 // Find and replace supports query syntax https://github.com/siyuan-note/siyuan/issues/14937 492 method = 0 493 } 494 495 if 0 != groupBy { 496 // 按文档分组后不支持替换 Need to be reminded that replacement operations are not supported after grouping by doc https://github.com/siyuan-note/siyuan/issues/10161 497 // 因为分组条件传入以后搜索只能命中文档块,会导致 全部替换 失效 498 err = errors.New(Conf.Language(221)) 499 return 500 } 501 502 // No longer trim spaces for the keyword and replacement https://github.com/siyuan-note/siyuan/issues/9229 503 if keyword == replacement { 504 return 505 } 506 507 r, _ := regexp.Compile(keyword) 508 escapedKey := util.EscapeHTML(keyword) 509 escapedKey = strings.ReplaceAll(escapedKey, "&#34;", "&quot;") 510 escapedKey = strings.ReplaceAll(escapedKey, "&#39;", "'") 511 escapedR, _ := regexp.Compile(escapedKey) 512 ids = gulu.Str.RemoveDuplicatedElem(ids) 513 var renameRoots []*ast.Node 514 renameRootTitles := map[string]string{} 515 cachedTrees := map[string]*parse.Tree{} 516 517 historyDir, err := getHistoryDir(HistoryOpReplace, time.Now()) 518 if err != nil { 519 logging.LogErrorf("get history dir failed: %s", err) 520 return 521 } 522 523 if 1 > len(ids) { 524 // `Replace All` is no longer affected by pagination https://github.com/siyuan-note/siyuan/issues/8265 525 blocks, _, _, _, _ := FullTextSearchBlock(keyword, boxes, paths, types, method, orderBy, groupBy, 1, math.MaxInt) 526 for _, block := range blocks { 527 ids = append(ids, block.ID) 528 } 529 } 530 531 for _, id := range ids { 532 bt := treenode.GetBlockTree(id) 533 if nil == bt { 534 continue 535 } 536 537 tree := cachedTrees[bt.RootID] 538 if nil != tree { 539 continue 540 } 541 542 tree, _ = LoadTreeByBlockID(id) 543 if nil == tree { 544 continue 545 } 546 547 historyPath := filepath.Join(historyDir, tree.Box, tree.Path) 548 if err = os.MkdirAll(filepath.Dir(historyPath), 0755); err != nil { 549 logging.LogErrorf("generate history failed: %s", err) 550 return 551 } 552 553 var data []byte 554 if data, err = filelock.ReadFile(filepath.Join(util.DataDir, tree.Box, tree.Path)); err != nil { 555 logging.LogErrorf("generate history failed: %s", err) 556 return 557 } 558 559 if err = gulu.File.WriteFileSafer(historyPath, data, 0644); err != nil { 560 logging.LogErrorf("generate history failed: %s", err) 561 return 562 } 563 564 cachedTrees[bt.RootID] = tree 565 } 566 indexHistoryDir(filepath.Base(historyDir), util.NewLute()) 567 568 luteEngine := util.NewLute() 569 var reloadTreeIDs []string 570 updateNodes := map[string]*ast.Node{} 571 for i, id := range ids { 572 bt := treenode.GetBlockTree(id) 573 if nil == bt { 574 continue 575 } 576 577 tree := cachedTrees[bt.RootID] 578 if nil == tree { 579 continue 580 } 581 582 node := treenode.GetNodeInTree(tree, id) 583 if nil == node { 584 continue 585 } 586 587 reloadTreeIDs = append(reloadTreeIDs, tree.ID) 588 if ast.NodeDocument == node.Type { 589 if !replaceTypes["docTitle"] { 590 continue 591 } 592 593 title := node.IALAttr("title") 594 tags := node.IALAttr("tags") 595 if 0 == method { 596 if strings.Contains(title, keyword) { 597 docTitleReplacement := strings.ReplaceAll(replacement, "/", "/") 598 renameRootTitles[node.ID] = strings.ReplaceAll(title, keyword, docTitleReplacement) 599 renameRoots = append(renameRoots, node) 600 } 601 602 if strings.Contains(tags, keyword) { 603 replacement = strings.TrimPrefix(replacement, "#") 604 replacement = strings.TrimSuffix(replacement, "#") 605 tags = strings.ReplaceAll(tags, keyword, replacement) 606 tags = strings.ReplaceAll(tags, editor.Zwsp, "") 607 node.SetIALAttr("tags", tags) 608 ReloadTag() 609 } 610 } else if 3 == method { 611 if nil != r && r.MatchString(title) { 612 docTitleReplacement := strings.ReplaceAll(replacement, "/", "/") 613 renameRootTitles[node.ID] = r.ReplaceAllString(title, docTitleReplacement) 614 renameRoots = append(renameRoots, node) 615 } 616 617 if nil != r && r.MatchString(tags) { 618 replacement = strings.TrimPrefix(replacement, "#") 619 replacement = strings.TrimSuffix(replacement, "#") 620 tags = r.ReplaceAllString(tags, replacement) 621 tags = strings.ReplaceAll(tags, editor.Zwsp, "") 622 node.SetIALAttr("tags", tags) 623 ReloadTag() 624 } 625 } 626 } else { 627 var unlinks []*ast.Node 628 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus { 629 if !entering { 630 return ast.WalkContinue 631 } 632 633 switch n.Type { 634 case ast.NodeText: 635 if !replaceTypes["text"] { 636 return ast.WalkContinue 637 } 638 639 if replaceTextNode(n, method, keyword, replacement, r, luteEngine) { 640 if nil != n.Parent && ast.NodeBackslash == n.Parent.Type { 641 unlinks = append(unlinks, n.Parent) 642 643 prev, next := n.Parent.Previous, n.Parent.Next 644 for ; prev != nil && ((ast.NodeText == prev.Type && prev.Tokens == nil) || ast.NodeBackslash == prev.Type); prev = prev.Previous { 645 // Tokens 为空的节点或者转义节点之前已经处理,需要跳过 646 } 647 if nil != prev && ast.NodeText == prev.Type && nil != next && ast.NodeText == next.Type { 648 prev.Tokens = append(prev.Tokens, next.Tokens...) 649 next.Tokens = nil // 将 Tokens 设置为空,表示该节点已经被处理过 650 unlinks = append(unlinks, next) 651 } 652 } else { 653 unlinks = append(unlinks, n) 654 } 655 } 656 case ast.NodeLinkDest: 657 if !replaceTypes["imgSrc"] { 658 return ast.WalkContinue 659 } 660 661 replaceNodeTokens(n, method, keyword, strings.TrimSpace(replacement), r) 662 if 1 > len(n.Tokens) { 663 unlinks = append(unlinks, n.Parent) 664 mergeSamePreNext(n) 665 } 666 case ast.NodeLinkText: 667 if !replaceTypes["imgText"] { 668 return ast.WalkContinue 669 } 670 671 replaceNodeTokens(n, method, keyword, replacement, r) 672 case ast.NodeLinkTitle: 673 if !replaceTypes["imgTitle"] { 674 return ast.WalkContinue 675 } 676 677 replaceNodeTokens(n, method, keyword, replacement, r) 678 case ast.NodeCodeBlockCode: 679 if !replaceTypes["codeBlock"] { 680 return ast.WalkContinue 681 } 682 683 replaceNodeTokens(n, method, keyword, replacement, r) 684 case ast.NodeMathBlockContent: 685 if !replaceTypes["mathBlock"] { 686 return ast.WalkContinue 687 } 688 689 replaceNodeTokens(n, method, keyword, replacement, r) 690 case ast.NodeHTMLBlock: 691 if !replaceTypes["htmlBlock"] { 692 return ast.WalkContinue 693 } 694 695 replaceNodeTokens(n, method, keyword, replacement, r) 696 case ast.NodeTextMark: 697 if n.IsTextMarkType("code") { 698 if !replaceTypes["code"] { 699 return ast.WalkContinue 700 } 701 702 if 0 == method { 703 if strings.Contains(n.TextMarkTextContent, escapedKey) { 704 n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, escapedKey, util.EscapeHTML(replacement)) 705 } 706 } else if 3 == method { 707 if nil != escapedR && escapedR.MatchString(n.TextMarkTextContent) { 708 n.TextMarkTextContent = escapedR.ReplaceAllString(n.TextMarkTextContent, util.EscapeHTML(replacement)) 709 } 710 } 711 712 if "" == n.TextMarkTextContent { 713 unlinks = append(unlinks, n) 714 mergeSamePreNext(n) 715 } 716 } else if n.IsTextMarkType("a") { 717 if replaceTypes["aText"] { 718 if 0 == method { 719 content := util.UnescapeHTML(n.TextMarkTextContent) 720 if strings.Contains(content, escapedKey) { 721 n.TextMarkTextContent = strings.ReplaceAll(content, escapedKey, replacement) 722 } else if strings.Contains(content, keyword) { 723 n.TextMarkTextContent = strings.ReplaceAll(content, keyword, replacement) 724 } 725 } else if 3 == method { 726 if nil != r && r.MatchString(n.TextMarkTextContent) { 727 n.TextMarkTextContent = r.ReplaceAllString(n.TextMarkTextContent, replacement) 728 } 729 } 730 if "" == n.TextMarkTextContent { 731 unlinks = append(unlinks, n) 732 mergeSamePreNext(n) 733 } 734 } 735 736 if replaceTypes["aTitle"] { 737 if 0 == method { 738 title := util.UnescapeHTML(n.TextMarkATitle) 739 if strings.Contains(title, escapedKey) { 740 n.TextMarkATitle = strings.ReplaceAll(title, escapedKey, replacement) 741 } else if strings.Contains(n.TextMarkATitle, keyword) { 742 n.TextMarkATitle = strings.ReplaceAll(title, keyword, replacement) 743 } 744 } else if 3 == method { 745 if nil != r && r.MatchString(n.TextMarkATitle) { 746 n.TextMarkATitle = r.ReplaceAllString(n.TextMarkATitle, replacement) 747 } 748 } 749 } 750 751 if replaceTypes["aHref"] { 752 if 0 == method { 753 href := util.UnescapeHTML(n.TextMarkAHref) 754 if strings.Contains(href, escapedKey) { 755 n.TextMarkAHref = strings.ReplaceAll(href, escapedKey, util.EscapeHTML(replacement)) 756 } else if strings.Contains(href, keyword) { 757 n.TextMarkAHref = strings.ReplaceAll(href, keyword, strings.TrimSpace(replacement)) 758 } 759 } else if 3 == method { 760 if nil != r && r.MatchString(n.TextMarkAHref) { 761 n.TextMarkAHref = r.ReplaceAllString(n.TextMarkAHref, strings.TrimSpace(replacement)) 762 } 763 } 764 765 if "" == n.TextMarkAHref { 766 if "" == n.TextMarkTextContent { 767 unlinks = append(unlinks, n) 768 mergeSamePreNext(n) 769 } else { 770 n.Type = ast.NodeText 771 n.Tokens = []byte(n.TextMarkTextContent) 772 } 773 } 774 } 775 } else if n.IsTextMarkType("em") { 776 if !replaceTypes["em"] { 777 return ast.WalkContinue 778 } 779 780 replaceNodeTextMarkTextContent(n, method, keyword, escapedKey, replacement, r, "em", luteEngine) 781 if "" == n.TextMarkTextContent { 782 unlinks = append(unlinks, n) 783 mergeSamePreNext(n) 784 } 785 } else if n.IsTextMarkType("strong") { 786 if !replaceTypes["strong"] { 787 return ast.WalkContinue 788 } 789 790 replaceNodeTextMarkTextContent(n, method, keyword, escapedKey, replacement, r, "strong", luteEngine) 791 if "" == n.TextMarkTextContent { 792 unlinks = append(unlinks, n) 793 mergeSamePreNext(n) 794 } 795 } else if n.IsTextMarkType("kbd") { 796 if !replaceTypes["kbd"] { 797 return ast.WalkContinue 798 } 799 800 replaceNodeTextMarkTextContent(n, method, keyword, escapedKey, replacement, r, "kbd", luteEngine) 801 if "" == n.TextMarkTextContent { 802 unlinks = append(unlinks, n) 803 } 804 } else if n.IsTextMarkType("mark") { 805 if !replaceTypes["mark"] { 806 return ast.WalkContinue 807 } 808 809 replaceNodeTextMarkTextContent(n, method, keyword, escapedKey, replacement, r, "mark", luteEngine) 810 if "" == n.TextMarkTextContent { 811 unlinks = append(unlinks, n) 812 mergeSamePreNext(n) 813 } 814 } else if n.IsTextMarkType("s") { 815 if !replaceTypes["s"] { 816 return ast.WalkContinue 817 } 818 819 replaceNodeTextMarkTextContent(n, method, keyword, escapedKey, replacement, r, "s", luteEngine) 820 if "" == n.TextMarkTextContent { 821 unlinks = append(unlinks, n) 822 mergeSamePreNext(n) 823 } 824 } else if n.IsTextMarkType("sub") { 825 if !replaceTypes["sub"] { 826 return ast.WalkContinue 827 } 828 829 replaceNodeTextMarkTextContent(n, method, keyword, escapedKey, replacement, r, "sub", luteEngine) 830 if "" == n.TextMarkTextContent { 831 unlinks = append(unlinks, n) 832 } 833 } else if n.IsTextMarkType("sup") { 834 if !replaceTypes["sup"] { 835 return ast.WalkContinue 836 } 837 838 replaceNodeTextMarkTextContent(n, method, keyword, escapedKey, replacement, r, "sup", luteEngine) 839 if "" == n.TextMarkTextContent { 840 unlinks = append(unlinks, n) 841 } 842 } else if n.IsTextMarkType("tag") { 843 if !replaceTypes["tag"] { 844 return ast.WalkContinue 845 } 846 847 replaceNodeTextMarkTextContent(n, method, keyword, escapedKey, replacement, r, "tag", luteEngine) 848 if "" == n.TextMarkTextContent { 849 unlinks = append(unlinks, n) 850 } 851 852 ReloadTag() 853 } else if n.IsTextMarkType("u") { 854 if !replaceTypes["u"] { 855 return ast.WalkContinue 856 } 857 858 replaceNodeTextMarkTextContent(n, method, keyword, escapedKey, replacement, r, "u", luteEngine) 859 if "" == n.TextMarkTextContent { 860 unlinks = append(unlinks, n) 861 mergeSamePreNext(n) 862 } 863 } else if n.IsTextMarkType("inline-math") { 864 if !replaceTypes["inlineMath"] { 865 return ast.WalkContinue 866 } 867 868 if 0 == method { 869 if strings.Contains(n.TextMarkInlineMathContent, keyword) { 870 n.TextMarkInlineMathContent = strings.ReplaceAll(n.TextMarkInlineMathContent, keyword, replacement) 871 } 872 } else if 3 == method { 873 if nil != r && r.MatchString(n.TextMarkInlineMathContent) { 874 n.TextMarkInlineMathContent = r.ReplaceAllString(n.TextMarkInlineMathContent, replacement) 875 } 876 } 877 878 if "" == n.TextMarkInlineMathContent { 879 unlinks = append(unlinks, n) 880 } 881 } else if n.IsTextMarkType("inline-memo") { 882 if !replaceTypes["inlineMemo"] { 883 return ast.WalkContinue 884 } 885 886 if 0 == method { 887 if strings.Contains(n.TextMarkInlineMemoContent, keyword) { 888 n.TextMarkInlineMemoContent = strings.ReplaceAll(n.TextMarkInlineMemoContent, keyword, replacement) 889 n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, keyword, replacement) 890 } 891 } else if 3 == method { 892 if nil != r && r.MatchString(n.TextMarkInlineMemoContent) { 893 n.TextMarkInlineMemoContent = r.ReplaceAllString(n.TextMarkInlineMemoContent, replacement) 894 n.TextMarkTextContent = r.ReplaceAllString(n.TextMarkTextContent, replacement) 895 } 896 } 897 898 if "" == n.TextMarkTextContent { 899 unlinks = append(unlinks, n) 900 } 901 } else if n.IsTextMarkType("text") { 902 // Search and replace fails in some cases https://github.com/siyuan-note/siyuan/issues/10016 903 if !replaceTypes["text"] { 904 return ast.WalkContinue 905 } 906 907 replaceNodeTextMarkTextContent(n, method, keyword, escapedKey, replacement, r, "text", luteEngine) 908 if "" == n.TextMarkTextContent { 909 unlinks = append(unlinks, n) 910 mergeSamePreNext(n) 911 } 912 } else if n.IsTextMarkType("block-ref") { 913 if !replaceTypes["blockRef"] { 914 return ast.WalkContinue 915 } 916 917 if 0 == method { 918 if strings.Contains(n.TextMarkTextContent, keyword) { 919 n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, keyword, replacement) 920 n.TextMarkBlockRefSubtype = "s" 921 } 922 } else if 3 == method { 923 if nil != r && r.MatchString(n.TextMarkTextContent) { 924 n.TextMarkTextContent = r.ReplaceAllString(n.TextMarkTextContent, replacement) 925 n.TextMarkBlockRefSubtype = "s" 926 } 927 } 928 929 if "" == n.TextMarkTextContent { 930 unlinks = append(unlinks, n) 931 } 932 } else if n.IsTextMarkType("file-annotation-ref") { 933 if !replaceTypes["fileAnnotationRef"] { 934 return ast.WalkContinue 935 } 936 937 if 0 == method { 938 if strings.Contains(n.TextMarkTextContent, keyword) { 939 n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, keyword, replacement) 940 } 941 } else if 3 == method { 942 if nil != r && r.MatchString(n.TextMarkTextContent) { 943 n.TextMarkTextContent = r.ReplaceAllString(n.TextMarkTextContent, replacement) 944 } 945 } 946 if "" == n.TextMarkTextContent { 947 unlinks = append(unlinks, n) 948 } 949 } 950 } 951 return ast.WalkContinue 952 }) 953 954 for _, unlink := range unlinks { 955 unlink.Unlink() 956 } 957 } 958 959 if err = writeTreeUpsertQueue(tree); err != nil { 960 return 961 } 962 updateNodes[id] = node 963 util.PushEndlessProgress(fmt.Sprintf(Conf.Language(206), i+1, len(ids))) 964 } 965 966 for i, renameRoot := range renameRoots { 967 newTitle := renameRootTitles[renameRoot.ID] 968 RenameDoc(renameRoot.Box, renameRoot.Path, newTitle) 969 970 util.PushEndlessProgress(fmt.Sprintf(Conf.Language(207), i+1, len(renameRoots))) 971 } 972 973 sql.FlushQueue() 974 975 reloadTreeIDs = gulu.Str.RemoveDuplicatedElem(reloadTreeIDs) 976 for _, id := range reloadTreeIDs { 977 ReloadProtyle(id) 978 } 979 980 updateAttributeViewBlockText(updateNodes) 981 982 sql.FlushQueue() 983 util.PushClearProgress() 984 return 985} 986 987func replaceNodeTextMarkTextContent(n *ast.Node, method int, keyword, escapedKey string, replacement string, r *regexp.Regexp, typ string, luteEngine *lute.Lute) { 988 if 0 == method { 989 if strings.Contains(typ, "tag") { 990 keyword = strings.TrimPrefix(keyword, "#") 991 keyword = strings.TrimSuffix(keyword, "#") 992 escapedKey = strings.TrimPrefix(escapedKey, "#") 993 escapedKey = strings.TrimSuffix(escapedKey, "#") 994 if strings.HasPrefix(replacement, "#") && strings.HasSuffix(replacement, "#") { 995 replacement = strings.TrimPrefix(replacement, "#") 996 replacement = strings.TrimSuffix(replacement, "#") 997 } else if n.TextMarkTextContent == keyword || n.TextMarkTextContent == escapedKey { 998 // 将标签转换为纯文本 999 1000 if "tag" == n.TextMarkType { // 没有其他类型,仅是标签时直接转换 1001 content := n.TextMarkTextContent 1002 if strings.Contains(content, escapedKey) { 1003 content = strings.ReplaceAll(content, escapedKey, replacement) 1004 } else if strings.Contains(content, keyword) { 1005 content = strings.ReplaceAll(content, keyword, replacement) 1006 } 1007 content = strings.ReplaceAll(content, editor.Zwsp, "") 1008 1009 tree := parse.Inline("", []byte(content), luteEngine.ParseOptions) 1010 if nil == tree.Root.FirstChild { 1011 return 1012 } 1013 parse.NestedInlines2FlattedSpans(tree, false) 1014 1015 var replaceNodes []*ast.Node 1016 for rNode := tree.Root.FirstChild.FirstChild; nil != rNode; rNode = rNode.Next { 1017 replaceNodes = append(replaceNodes, rNode) 1018 if blockRefID, _, _ := treenode.GetBlockRef(rNode); "" != blockRefID { 1019 task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, blockRefID) 1020 } 1021 } 1022 1023 for _, rNode := range replaceNodes { 1024 n.InsertBefore(rNode) 1025 } 1026 n.TextMarkTextContent = "" 1027 return 1028 } 1029 1030 // 存在其他类型时仅移除标签类型 1031 n.TextMarkType = strings.ReplaceAll(n.TextMarkType, "tag", "") 1032 n.TextMarkType = strings.TrimSpace(n.TextMarkType) 1033 } else if strings.Contains(n.TextMarkTextContent, keyword) || strings.Contains(n.TextMarkTextContent, escapedKey) { // 标签包含了部分关键字的情况 1034 if "tag" == n.TextMarkType { // 没有其他类型,仅是标签时保持标签类型不变,仅替换标签部分内容 1035 content := n.TextMarkTextContent 1036 if strings.Contains(content, escapedKey) { 1037 content = strings.ReplaceAll(content, escapedKey, replacement) 1038 } else if strings.Contains(content, keyword) { 1039 content = strings.ReplaceAll(content, keyword, replacement) 1040 } 1041 content = strings.ReplaceAll(content, editor.Zwsp, "") 1042 n.TextMarkTextContent = content 1043 return 1044 } 1045 } 1046 } 1047 1048 if strings.Contains(n.TextMarkTextContent, escapedKey) { 1049 n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, escapedKey, util.EscapeHTML(replacement)) 1050 } else if strings.Contains(n.TextMarkTextContent, keyword) { 1051 n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, keyword, replacement) 1052 } 1053 n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, editor.Zwsp, "") 1054 } else if 3 == method { 1055 if nil != r && r.MatchString(n.TextMarkTextContent) { 1056 n.TextMarkTextContent = r.ReplaceAllString(n.TextMarkTextContent, replacement) 1057 } 1058 n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, editor.Zwsp, "") 1059 } 1060} 1061 1062// replaceTextNode 替换文本节点为其他节点。 1063// Supports replacing text elements with other elements https://github.com/siyuan-note/siyuan/issues/11058 1064func replaceTextNode(text *ast.Node, method int, keyword string, replacement string, r *regexp.Regexp, luteEngine *lute.Lute) bool { 1065 if 0 == method { 1066 newContent := text.Tokens 1067 if Conf.Search.CaseSensitive { 1068 if bytes.Contains(text.Tokens, []byte(keyword)) { 1069 newContent = bytes.ReplaceAll(text.Tokens, []byte(keyword), []byte(replacement)) 1070 } 1071 } else { 1072 if "" != strings.TrimSpace(keyword) { 1073 // 当搜索结果中的文本元素包含大小写混合时替换失败 1074 // Replace fails when search results contain mixed case in text elements https://github.com/siyuan-note/siyuan/issues/9171 1075 keywords := strings.Split(keyword, " ") 1076 // keyword 可能是 "foo Foo" 使用空格分隔的大小写命中情况,这里统一转换小写后去重 1077 if 0 < len(keywords) { 1078 var lowerKeywords []string 1079 for _, k := range keywords { 1080 lowerKeywords = append(lowerKeywords, strings.ToLower(k)) 1081 } 1082 keyword = strings.Join(lowerKeywords, " ") 1083 } 1084 } 1085 1086 if bytes.Contains(bytes.ToLower(text.Tokens), []byte(keyword)) { 1087 newContent = replaceCaseInsensitive(text.Tokens, []byte(keyword), []byte(replacement)) 1088 } 1089 } 1090 if !bytes.Equal(newContent, text.Tokens) { 1091 tree := parse.Inline("", newContent, luteEngine.ParseOptions) 1092 if nil == tree.Root.FirstChild { 1093 return false 1094 } 1095 parse.NestedInlines2FlattedSpans(tree, false) 1096 1097 var replaceNodes []*ast.Node 1098 for rNode := tree.Root.FirstChild.FirstChild; nil != rNode; rNode = rNode.Next { 1099 replaceNodes = append(replaceNodes, rNode) 1100 } 1101 1102 for _, rNode := range replaceNodes { 1103 text.InsertBefore(rNode) 1104 } 1105 return true 1106 } 1107 } else if 3 == method { 1108 if nil != r && r.MatchString(string(text.Tokens)) { 1109 newContent := []byte(r.ReplaceAllString(string(text.Tokens), replacement)) 1110 tree := parse.Inline("", newContent, luteEngine.ParseOptions) 1111 if nil == tree.Root.FirstChild { 1112 return false 1113 } 1114 1115 var replaceNodes []*ast.Node 1116 for rNode := tree.Root.FirstChild.FirstChild; nil != rNode; rNode = rNode.Next { 1117 replaceNodes = append(replaceNodes, rNode) 1118 } 1119 1120 for _, rNode := range replaceNodes { 1121 text.InsertBefore(rNode) 1122 } 1123 return true 1124 } 1125 } 1126 return false 1127} 1128 1129func replaceNodeTokens(n *ast.Node, method int, keyword string, replacement string, r *regexp.Regexp) { 1130 if 0 == method { 1131 if bytes.Contains(n.Tokens, []byte(keyword)) { 1132 n.Tokens = bytes.ReplaceAll(n.Tokens, []byte(keyword), []byte(replacement)) 1133 } 1134 } else if 3 == method { 1135 if nil != r && r.MatchString(string(n.Tokens)) { 1136 n.Tokens = []byte(r.ReplaceAllString(string(n.Tokens), replacement)) 1137 } 1138 } 1139} 1140 1141func mergeSamePreNext(n *ast.Node) { 1142 prev, next := n.Previous, n.Next 1143 if nil != n.Parent && ast.NodeImage == n.Parent.Type { 1144 prev = n.Parent.Previous 1145 next = n.Parent.Next 1146 } 1147 1148 if nil == prev || nil == next || prev.Type != next.Type || ast.NodeKramdownSpanIAL == prev.Type { 1149 return 1150 } 1151 1152 switch prev.Type { 1153 case ast.NodeText: 1154 prev.Tokens = append(prev.Tokens, next.Tokens...) 1155 next.Unlink() 1156 case ast.NodeTextMark: 1157 if prev.TextMarkType != next.TextMarkType { 1158 break 1159 } 1160 1161 switch prev.TextMarkType { 1162 case "em", "strong", "mark", "s", "u", "text": 1163 prev.TextMarkTextContent += next.TextMarkTextContent 1164 next.Unlink() 1165 } 1166 } 1167} 1168 1169// FullTextSearchBlock 搜索内容块。 1170// 1171// method:0:关键字,1:查询语法,2:SQL,3:正则表达式 1172// orderBy: 0:按块类型(默认),1:按创建时间升序,2:按创建时间降序,3:按更新时间升序,4:按更新时间降序,5:按内容顺序(仅在按文档分组时),6:按相关度升序,7:按相关度降序 1173// groupBy:0:不分组,1:按文档分组 1174func FullTextSearchBlock(query string, boxes, paths []string, types map[string]bool, method, orderBy, groupBy, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount, pageCount int, docMode bool) { 1175 ret = []*Block{} 1176 if "" == query { 1177 return 1178 } 1179 1180 query = filterQueryInvisibleChars(query) 1181 var ignoreFilter string 1182 if ignoreLines := getSearchIgnoreLines(); 0 < len(ignoreLines) { 1183 // Support ignore search results https://github.com/siyuan-note/siyuan/issues/10089 1184 buf := bytes.Buffer{} 1185 for _, line := range ignoreLines { 1186 buf.WriteString(" AND ") 1187 buf.WriteString(line) 1188 } 1189 ignoreFilter += buf.String() 1190 } 1191 1192 beforeLen := 36 1193 var blocks []*Block 1194 orderByClause := buildOrderBy(query, method, orderBy) 1195 switch method { 1196 case 1: // 查询语法 1197 typeFilter := buildTypeFilter(types) 1198 boxFilter := buildBoxesFilter(boxes) 1199 pathFilter := buildPathsFilter(paths) 1200 if ast.IsNodeIDPattern(query) { 1201 blocks, matchedBlockCount, matchedRootCount = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+query+"'", beforeLen, page, pageSize) 1202 } else { 1203 blocks, matchedBlockCount, matchedRootCount = fullTextSearchByFTS(query, boxFilter, pathFilter, typeFilter, ignoreFilter, orderByClause, beforeLen, page, pageSize) 1204 } 1205 case 2: // SQL 1206 blocks, matchedBlockCount, matchedRootCount = searchBySQL(query, beforeLen, page, pageSize) 1207 case 3: // 正则表达式 1208 typeFilter := buildTypeFilter(types) 1209 boxFilter := buildBoxesFilter(boxes) 1210 pathFilter := buildPathsFilter(paths) 1211 blocks, matchedBlockCount, matchedRootCount = fullTextSearchByRegexp(query, boxFilter, pathFilter, typeFilter, ignoreFilter, orderByClause, beforeLen, page, pageSize) 1212 default: // 关键字 1213 typeFilter := buildTypeFilter(types) 1214 boxFilter := buildBoxesFilter(boxes) 1215 pathFilter := buildPathsFilter(paths) 1216 if ast.IsNodeIDPattern(query) { 1217 blocks, matchedBlockCount, matchedRootCount = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+query+"'", beforeLen, page, pageSize) 1218 } else { 1219 if 2 > len(strings.Split(strings.TrimSpace(query), " ")) { 1220 query = stringQuery(query) 1221 blocks, matchedBlockCount, matchedRootCount = fullTextSearchByFTS(query, boxFilter, pathFilter, typeFilter, ignoreFilter, orderByClause, beforeLen, page, pageSize) 1222 } else { 1223 docMode = true // 文档全文搜索模式 https://github.com/siyuan-note/siyuan/issues/10584 1224 blocks, matchedBlockCount, matchedRootCount = fullTextSearchByLikeWithRoot(query, boxFilter, pathFilter, typeFilter, ignoreFilter, orderByClause, beforeLen, page, pageSize) 1225 } 1226 } 1227 } 1228 pageCount = (matchedBlockCount + pageSize - 1) / pageSize 1229 1230 switch groupBy { 1231 case 0: // 不分组 1232 ret = blocks 1233 case 1: // 按文档分组 1234 rootMap := map[string]bool{} 1235 var rootIDs []string 1236 contentSorts := map[string]int{} 1237 var btsID []string 1238 for _, b := range blocks { 1239 btsID = append(btsID, b.RootID) 1240 } 1241 btsID = gulu.Str.RemoveDuplicatedElem(btsID) 1242 bts := treenode.GetBlockTrees(btsID) 1243 for _, b := range blocks { 1244 if _, ok := rootMap[b.RootID]; !ok { 1245 rootMap[b.RootID] = true 1246 rootIDs = append(rootIDs, b.RootID) 1247 tree, _ := loadTreeByBlockTree(bts[b.RootID]) 1248 if nil == tree { 1249 continue 1250 } 1251 1252 if 5 == orderBy { // 按内容顺序(仅在按文档分组时) 1253 sortVal := 0 1254 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 1255 if !entering || !n.IsBlock() { 1256 return ast.WalkContinue 1257 } 1258 1259 contentSorts[n.ID] = sortVal 1260 sortVal++ 1261 return ast.WalkContinue 1262 }) 1263 } 1264 } 1265 } 1266 1267 sqlRoots := sql.GetBlocks(rootIDs) 1268 roots := fromSQLBlocks(&sqlRoots, "", beforeLen) 1269 for _, root := range roots { 1270 for _, b := range blocks { 1271 if 5 == orderBy { // 按内容顺序(仅在按文档分组时) 1272 b.Sort = contentSorts[b.ID] 1273 } 1274 if b.RootID == root.ID { 1275 root.Children = append(root.Children, b) 1276 } 1277 } 1278 1279 switch orderBy { 1280 case 1: //按创建时间升序 1281 sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Created < root.Children[j].Created }) 1282 case 2: // 按创建时间降序 1283 sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Created > root.Children[j].Created }) 1284 case 3: // 按更新时间升序 1285 sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Updated < root.Children[j].Updated }) 1286 case 4: // 按更新时间降序 1287 sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Updated > root.Children[j].Updated }) 1288 case 5: // 按内容顺序(仅在按文档分组时) 1289 sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Sort < root.Children[j].Sort }) 1290 default: // 按块类型(默认) 1291 sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Sort < root.Children[j].Sort }) 1292 } 1293 } 1294 1295 switch orderBy { 1296 case 1: //按创建时间升序 1297 sort.Slice(roots, func(i, j int) bool { return roots[i].Created < roots[j].Created }) 1298 case 2: // 按创建时间降序 1299 sort.Slice(roots, func(i, j int) bool { return roots[i].Created > roots[j].Created }) 1300 case 3: // 按更新时间升序 1301 sort.Slice(roots, func(i, j int) bool { return roots[i].Updated < roots[j].Updated }) 1302 case 4: // 按更新时间降序 1303 sort.Slice(roots, func(i, j int) bool { return roots[i].Updated > roots[j].Updated }) 1304 case 5: // 按内容顺序(仅在按文档分组时) 1305 // 都是文档,按更新时间降序 1306 sort.Slice(roots, func(i, j int) bool { return roots[i].IAL["updated"] > roots[j].IAL["updated"] }) 1307 case 6, 7: // 按相关度 1308 // 已在 ORDER BY 中处理 1309 default: // 按块类型(默认) 1310 // 都是文档,不需要再次排序 1311 } 1312 ret = roots 1313 default: 1314 ret = blocks 1315 } 1316 if 1 > len(ret) { 1317 ret = []*Block{} 1318 } 1319 1320 if 0 == groupBy { 1321 filterSelfHPath(ret) 1322 } 1323 1324 var nodeIDs []string 1325 for _, b := range ret { 1326 if 0 == groupBy { 1327 nodeIDs = append(nodeIDs, b.ID) 1328 } else { 1329 for _, c := range b.Children { 1330 nodeIDs = append(nodeIDs, c.ID) 1331 } 1332 } 1333 } 1334 1335 refCount := sql.QueryRefCount(nodeIDs) 1336 for _, b := range ret { 1337 if 0 == groupBy { 1338 b.RefCount = refCount[b.ID] 1339 } else { 1340 for _, c := range b.Children { 1341 c.RefCount = refCount[c.ID] 1342 } 1343 } 1344 } 1345 return 1346} 1347 1348func buildBoxesFilter(boxes []string) string { 1349 if 0 == len(boxes) { 1350 return "" 1351 } 1352 builder := bytes.Buffer{} 1353 builder.WriteString(" AND (") 1354 for i, box := range boxes { 1355 builder.WriteString(fmt.Sprintf("box = '%s'", box)) 1356 if i < len(boxes)-1 { 1357 builder.WriteString(" OR ") 1358 } 1359 } 1360 builder.WriteString(")") 1361 return builder.String() 1362} 1363 1364func buildPathsFilter(paths []string) string { 1365 if 0 == len(paths) { 1366 return "" 1367 } 1368 builder := bytes.Buffer{} 1369 builder.WriteString(" AND (") 1370 for i, path := range paths { 1371 builder.WriteString(fmt.Sprintf("path LIKE '%s%%'", path)) 1372 if i < len(paths)-1 { 1373 builder.WriteString(" OR ") 1374 } 1375 } 1376 builder.WriteString(")") 1377 return builder.String() 1378} 1379 1380func buildOrderBy(query string, method, orderBy int) string { 1381 switch orderBy { 1382 case 1: 1383 return "ORDER BY created ASC" 1384 case 2: 1385 return "ORDER BY created DESC" 1386 case 3: 1387 return "ORDER BY updated ASC" 1388 case 4: 1389 return "ORDER BY updated DESC" 1390 case 6: 1391 if 0 != method && 1 != method { 1392 // 只有关键字搜索和查询语法搜索才支持按相关度升序 https://github.com/siyuan-note/siyuan/issues/7861 1393 return "ORDER BY sort DESC, updated DESC" 1394 } 1395 return "ORDER BY rank DESC" // 默认是按相关度降序,所以按相关度升序要反过来使用 DESC 1396 case 7: 1397 if 0 != method && 1 != method { 1398 return "ORDER BY sort ASC, updated DESC" 1399 } 1400 return "ORDER BY rank" // 默认是按相关度降序 1401 default: 1402 clause := "ORDER BY CASE " + 1403 "WHEN name = '${keyword}' THEN 10 " + 1404 "WHEN alias = '${keyword}' THEN 20 " + 1405 "WHEN name LIKE '%${keyword}%' THEN 50 " + 1406 "WHEN alias LIKE '%${keyword}%' THEN 60 " + 1407 "ELSE 65535 END ASC, sort ASC, updated DESC" 1408 clause = strings.ReplaceAll(clause, "${keyword}", strings.ReplaceAll(query, "'", "''")) 1409 return clause 1410 } 1411} 1412 1413func buildTypeFilter(types map[string]bool) string { 1414 s := conf.NewSearch() 1415 if err := copier.Copy(s, Conf.Search); err != nil { 1416 logging.LogErrorf("copy search conf failed: %s", err) 1417 } 1418 if nil != types { 1419 s.Document = types["document"] 1420 s.Heading = types["heading"] 1421 s.List = types["list"] 1422 s.ListItem = types["listItem"] 1423 s.CodeBlock = types["codeBlock"] 1424 s.MathBlock = types["mathBlock"] 1425 s.Table = types["table"] 1426 s.Blockquote = types["blockquote"] 1427 s.SuperBlock = types["superBlock"] 1428 s.Paragraph = types["paragraph"] 1429 s.HTMLBlock = types["htmlBlock"] 1430 s.EmbedBlock = types["embedBlock"] 1431 s.DatabaseBlock = types["databaseBlock"] 1432 s.AudioBlock = types["audioBlock"] 1433 s.VideoBlock = types["videoBlock"] 1434 s.IFrameBlock = types["iframeBlock"] 1435 s.WidgetBlock = types["widgetBlock"] 1436 } else { 1437 s.Document = Conf.Search.Document 1438 s.Heading = Conf.Search.Heading 1439 s.List = Conf.Search.List 1440 s.ListItem = Conf.Search.ListItem 1441 s.CodeBlock = Conf.Search.CodeBlock 1442 s.MathBlock = Conf.Search.MathBlock 1443 s.Table = Conf.Search.Table 1444 s.Blockquote = Conf.Search.Blockquote 1445 s.SuperBlock = Conf.Search.SuperBlock 1446 s.Paragraph = Conf.Search.Paragraph 1447 s.HTMLBlock = Conf.Search.HTMLBlock 1448 s.EmbedBlock = Conf.Search.EmbedBlock 1449 s.DatabaseBlock = Conf.Search.DatabaseBlock 1450 s.AudioBlock = Conf.Search.AudioBlock 1451 s.VideoBlock = Conf.Search.VideoBlock 1452 s.IFrameBlock = Conf.Search.IFrameBlock 1453 s.WidgetBlock = Conf.Search.WidgetBlock 1454 } 1455 return s.TypeFilter() 1456} 1457 1458func searchBySQL(stmt string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) { 1459 stmt = strings.TrimSpace(stmt) 1460 blocks := sql.SelectBlocksRawStmt(stmt, page, pageSize) 1461 ret = fromSQLBlocks(&blocks, "", beforeLen) 1462 if 1 > len(ret) { 1463 ret = []*Block{} 1464 return 1465 } 1466 1467 stmt = strings.ToLower(stmt) 1468 stdQuery := !strings.Contains(stmt, "with recursive") && !strings.Contains(stmt, "union") 1469 if stdQuery { 1470 if strings.HasPrefix(stmt, "select a.* ") { // 多个搜索关键字匹配文档 https://github.com/siyuan-note/siyuan/issues/7350 1471 stmt = strings.ReplaceAll(stmt, "select a.* ", "select COUNT(a.id) AS `matches`, COUNT(DISTINCT(a.root_id)) AS `docs` ") 1472 } else { 1473 stmt = strings.ReplaceAll(stmt, "select * ", "select COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` ") 1474 } 1475 } 1476 stmt = removeLimitClause(stmt) 1477 result, _ := sql.QueryNoLimit(stmt) 1478 if 1 > len(result) { 1479 return 1480 } 1481 1482 if !stdQuery { 1483 var rootIDs, blockIDs []string 1484 for _, queryResult := range result { 1485 rootIDs = append(rootIDs, queryResult["root_id"].(string)) 1486 blockIDs = append(blockIDs, queryResult["id"].(string)) 1487 } 1488 rootIDs = gulu.Str.RemoveDuplicatedElem(rootIDs) 1489 blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs) 1490 matchedRootCount = len(rootIDs) 1491 matchedBlockCount = len(blockIDs) 1492 } else { 1493 matchedBlockCount = int(result[0]["matches"].(int64)) 1494 matchedRootCount = int(result[0]["docs"].(int64)) 1495 } 1496 return 1497} 1498 1499func removeLimitClause(stmt string) string { 1500 parsedStmt, err := sqlparser.Parse(stmt) 1501 if err != nil { 1502 return stmt 1503 } 1504 1505 switch parsedStmt.(type) { 1506 case *sqlparser.Select: 1507 slct := parsedStmt.(*sqlparser.Select) 1508 if nil != slct.Limit { 1509 slct.Limit = nil 1510 } 1511 stmt = sqlparser.String(slct) 1512 } 1513 return stmt 1514} 1515 1516func fullTextSearchRefBlock(keyword string, beforeLen int, onlyDoc bool) (ret []*Block) { 1517 keyword = filterQueryInvisibleChars(keyword) 1518 1519 if id := extractID(keyword); "" != id { 1520 ret, _, _ = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+id+"'", 36, 1, 32) 1521 return 1522 } 1523 1524 quotedKeyword := stringQuery(keyword) 1525 table := "blocks_fts" // 大小写敏感 1526 if !Conf.Search.CaseSensitive { 1527 table = "blocks_fts_case_insensitive" 1528 } 1529 1530 projections := "id, parent_id, root_id, hash, box, path, " + 1531 "snippet(" + table + ", 6, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS hpath, " + 1532 "snippet(" + table + ", 7, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS name, " + 1533 "snippet(" + table + ", 8, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS alias, " + 1534 "snippet(" + table + ", 9, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS memo, " + 1535 "snippet(" + table + ", 10, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS tag, " + 1536 "snippet(" + table + ", 11, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS content, " + 1537 "fcontent, markdown, length, type, subtype, ial, sort, created, updated" 1538 stmt := "SELECT " + projections + " FROM " + table + " WHERE " + table + " MATCH '" + columnFilter() + ":(" + quotedKeyword + ")' AND type" 1539 if onlyDoc { 1540 stmt += " = 'd'" 1541 } else { 1542 stmt += " IN " + Conf.Search.TypeFilter() 1543 } 1544 1545 if ignoreLines := getRefSearchIgnoreLines(); 0 < len(ignoreLines) { 1546 // Support ignore search results https://github.com/siyuan-note/siyuan/issues/10089 1547 buf := bytes.Buffer{} 1548 for _, line := range ignoreLines { 1549 buf.WriteString(" AND ") 1550 buf.WriteString(line) 1551 } 1552 stmt += buf.String() 1553 } 1554 1555 orderBy := ` ORDER BY CASE 1556 WHEN name = '${keyword}' THEN 10 1557 WHEN alias = '${keyword}' THEN 20 1558 WHEN memo = '${keyword}' THEN 30 1559 WHEN content = '${keyword}' and type = 'd' THEN 40 1560 WHEN content LIKE '%${keyword}%' and type = 'd' THEN 41 1561 WHEN name LIKE '%${keyword}%' THEN 50 1562 WHEN alias LIKE '%${keyword}%' THEN 60 1563 WHEN content = '${keyword}' and type = 'h' THEN 70 1564 WHEN content LIKE '%${keyword}%' and type = 'h' THEN 71 1565 WHEN fcontent = '${keyword}' and type = 'i' THEN 80 1566 WHEN fcontent LIKE '%${keyword}%' and type = 'i' THEN 81 1567 WHEN memo LIKE '%${keyword}%' THEN 90 1568 WHEN content LIKE '%${keyword}%' and type != 'i' and type != 'l' THEN 100 1569 ELSE 65535 END ASC, sort ASC, length ASC` 1570 orderBy = strings.ReplaceAll(orderBy, "${keyword}", strings.ReplaceAll(keyword, "'", "''")) 1571 stmt += orderBy + " LIMIT " + strconv.Itoa(Conf.Search.Limit) 1572 blocks := sql.SelectBlocksRawStmtNoParse(stmt, Conf.Search.Limit) 1573 ret = fromSQLBlocks(&blocks, "", beforeLen) 1574 if 1 > len(ret) { 1575 ret = []*Block{} 1576 } 1577 return 1578} 1579 1580func extractID(content string) (ret string) { 1581 // Improve block ref search ID extraction https://github.com/siyuan-note/siyuan/issues/10848 1582 1583 if 22 > len(content) { 1584 return 1585 } 1586 1587 // 从第一个字符开始循环,直到找到一个合法的 ID 为止 1588 for i := 0; i < len(content)-21; i++ { 1589 if ast.IsNodeIDPattern(content[i : i+22]) { 1590 ret = content[i : i+22] 1591 return 1592 } 1593 } 1594 return 1595} 1596 1597func fullTextSearchByRegexp(exp, boxFilter, pathFilter, typeFilter, ignoreFilter, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) { 1598 fieldFilter := fieldRegexp(exp) 1599 stmt := "SELECT * FROM `blocks` WHERE " + fieldFilter + " AND type IN " + typeFilter 1600 stmt += boxFilter + pathFilter + ignoreFilter + " " + orderBy 1601 regex, err := regexp.Compile(exp) 1602 if nil != err { 1603 util.PushErrMsg(err.Error(), 5000) 1604 return 1605 } 1606 1607 blocks := sql.SelectBlocksRegex(stmt, regex, Conf.Search.Name, Conf.Search.Alias, Conf.Search.Memo, Conf.Search.IAL, page, pageSize) 1608 ret = fromSQLBlocks(&blocks, "", beforeLen) 1609 if 1 > len(ret) { 1610 ret = []*Block{} 1611 } 1612 1613 matchedBlockCount, matchedRootCount = fullTextSearchCountByRegexp(exp, boxFilter, pathFilter, typeFilter, ignoreFilter) 1614 return 1615} 1616 1617func fullTextSearchCountByRegexp(exp, boxFilter, pathFilter, typeFilter, ignoreFilter string) (matchedBlockCount, matchedRootCount int) { 1618 fieldFilter := fieldRegexp(exp) 1619 stmt := "SELECT COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` FROM `blocks` WHERE " + fieldFilter + " AND type IN " + typeFilter + ignoreFilter 1620 stmt += boxFilter + pathFilter 1621 result, _ := sql.QueryNoLimit(stmt) 1622 if 1 > len(result) { 1623 return 1624 } 1625 matchedBlockCount = int(result[0]["matches"].(int64)) 1626 matchedRootCount = int(result[0]["docs"].(int64)) 1627 return 1628} 1629 1630func fullTextSearchByFTS(query, boxFilter, pathFilter, typeFilter, ignoreFilter, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) { 1631 table := "blocks_fts" // 大小写敏感 1632 if !Conf.Search.CaseSensitive { 1633 table = "blocks_fts_case_insensitive" 1634 } 1635 projections := "id, parent_id, root_id, hash, box, path, " + 1636 // Search result content snippet returns more text https://github.com/siyuan-note/siyuan/issues/10707 1637 "snippet(" + table + ", 6, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 512) AS hpath, " + 1638 "snippet(" + table + ", 7, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 512) AS name, " + 1639 "snippet(" + table + ", 8, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 512) AS alias, " + 1640 "snippet(" + table + ", 9, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 512) AS memo, " + 1641 "snippet(" + table + ", 10, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS tag, " + 1642 "snippet(" + table + ", 11, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 512) AS content, " + 1643 "fcontent, markdown, length, type, subtype, ial, sort, created, updated" 1644 stmt := "SELECT " + projections + " FROM " + table + " WHERE (`" + table + "` MATCH '" + columnFilter() + ":(" + query + ")'" 1645 stmt += ") AND type IN " + typeFilter 1646 stmt += boxFilter + pathFilter + ignoreFilter + " " + orderBy 1647 stmt += " LIMIT " + strconv.Itoa(pageSize) + " OFFSET " + strconv.Itoa((page-1)*pageSize) 1648 blocks := sql.SelectBlocksRawStmt(stmt, page, pageSize) 1649 ret = fromSQLBlocks(&blocks, "", beforeLen) 1650 if 1 > len(ret) { 1651 ret = []*Block{} 1652 } 1653 1654 matchedBlockCount, matchedRootCount = fullTextSearchCountByFTS(query, boxFilter, pathFilter, typeFilter, ignoreFilter) 1655 return 1656} 1657 1658func fullTextSearchCountByFTS(query, boxFilter, pathFilter, typeFilter, ignoreFilter string) (matchedBlockCount, matchedRootCount int) { 1659 table := "blocks_fts" // 大小写敏感 1660 if !Conf.Search.CaseSensitive { 1661 table = "blocks_fts_case_insensitive" 1662 } 1663 1664 stmt := "SELECT COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` FROM `" + table + "` WHERE (`" + table + "` MATCH '" + columnFilter() + ":(" + query + ")'" 1665 stmt += ") AND type IN " + typeFilter 1666 stmt += boxFilter + pathFilter + ignoreFilter 1667 result, _ := sql.QueryNoLimit(stmt) 1668 if 1 > len(result) { 1669 return 1670 } 1671 matchedBlockCount = int(result[0]["matches"].(int64)) 1672 matchedRootCount = int(result[0]["docs"].(int64)) 1673 return 1674} 1675 1676func fullTextSearchByLikeWithRoot(query, boxFilter, pathFilter, typeFilter, ignoreFilter, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) { 1677 query = strings.ReplaceAll(query, "'", "''") // 不需要转义双引号,因为条件都是通过单引号包裹的,只需要转义单引号即可 1678 keywords := strings.Split(query, " ") 1679 contentField := columnConcat() 1680 var likeFilter string 1681 orderByLike := "(" 1682 for i, keyword := range keywords { 1683 likeFilter += "GROUP_CONCAT(" + contentField + ") LIKE '%" + keyword + "%'" 1684 orderByLike += "(docContent LIKE '%" + keyword + "%')" 1685 if i < len(keywords)-1 { 1686 likeFilter += " AND " 1687 orderByLike += " + " 1688 } 1689 } 1690 orderByLike += ")" 1691 dMatchStmt := "SELECT root_id, MAX(CASE WHEN type = 'd' THEN (" + contentField + ") END) AS docContent" + 1692 " FROM blocks WHERE type IN " + typeFilter + boxFilter + pathFilter + ignoreFilter + 1693 " GROUP BY root_id HAVING " + likeFilter + "ORDER BY " + orderByLike + " DESC, MAX(updated) DESC" 1694 cteStmt := "WITH docBlocks AS (" + dMatchStmt + ")" 1695 likeFilter = strings.ReplaceAll(likeFilter, "GROUP_CONCAT("+contentField+")", "concatContent") 1696 limit := " LIMIT " + strconv.Itoa(pageSize) + " OFFSET " + strconv.Itoa((page-1)*pageSize) 1697 selectStmt := cteStmt + "\nSELECT *, " + 1698 "(" + contentField + ") AS concatContent, " + 1699 "(SELECT COUNT(root_id) FROM docBlocks) AS docs, " + 1700 "(CASE WHEN (root_id IN (SELECT root_id FROM docBlocks) AND (" + strings.ReplaceAll(likeFilter, "concatContent", contentField) + ")) THEN 1 ELSE 0 END) AS blockSort" + 1701 " FROM blocks WHERE type IN " + typeFilter + boxFilter + pathFilter + ignoreFilter + 1702 " AND (id IN (SELECT root_id FROM docBlocks " + limit + ") OR" + 1703 " (root_id IN (SELECT root_id FROM docBlocks" + limit + ") AND (" + likeFilter + ")))" 1704 if strings.Contains(orderBy, "ORDER BY rank DESC") { 1705 orderBy = buildOrderBy(query, 0, 0) 1706 selectStmt += " " + strings.Replace(orderBy, "END ASC, ", "END ASC, blockSort ASC, ", 1) 1707 } else if strings.Contains(orderBy, "ORDER BY rank") { 1708 orderBy = buildOrderBy(query, 0, 0) 1709 selectStmt += " " + strings.Replace(orderBy, "END ASC, ", "END ASC, blockSort DESC, ", 1) 1710 } else if strings.Contains(orderBy, "sort ASC") { 1711 selectStmt += " " + strings.Replace(orderBy, "END ASC, ", "END ASC, blockSort DESC, ", 1) 1712 } else { 1713 selectStmt += " " + orderBy 1714 } 1715 result, _ := sql.QueryNoLimit(selectStmt) 1716 resultBlocks := sql.ToBlocks(result) 1717 if 0 < len(resultBlocks) { 1718 matchedRootCount = int(result[0]["docs"].(int64)) 1719 matchedBlockCount = matchedRootCount 1720 } 1721 1722 keywords = gulu.Str.RemoveDuplicatedElem(keywords) 1723 terms := strings.Join(keywords, search.TermSep) 1724 terms = strings.ReplaceAll(terms, "''", "'") 1725 ret = fromSQLBlocks(&resultBlocks, terms, beforeLen) 1726 if 1 > len(ret) { 1727 ret = []*Block{} 1728 } 1729 return 1730} 1731 1732func highlightByFTS(query, typeFilter, id string) (ret []string) { 1733 query = strings.ReplaceAll(query, " ", " OR ") 1734 const limit = 256 1735 table := "blocks_fts" 1736 if !Conf.Search.CaseSensitive { 1737 table = "blocks_fts_case_insensitive" 1738 } 1739 projections := "id, parent_id, root_id, hash, box, path, " + 1740 "highlight(" + table + ", 6, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS hpath, " + 1741 "highlight(" + table + ", 7, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS name, " + 1742 "highlight(" + table + ", 8, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS alias, " + 1743 "highlight(" + table + ", 9, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS memo, " + 1744 "highlight(" + table + ", 10, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS tag, " + 1745 "highlight(" + table + ", 11, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS content, " + 1746 "fcontent, markdown, length, type, subtype, " + 1747 "highlight(" + table + ", 17, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS ial, " + 1748 "sort, created, updated" 1749 stmt := "SELECT " + projections + " FROM " + table + " WHERE (`" + table + "` MATCH '" + columnFilter() + ":(" + query + ")'" 1750 stmt += ") AND type IN " + typeFilter 1751 stmt += " AND root_id = '" + id + "'" 1752 stmt += " LIMIT " + strconv.Itoa(limit) 1753 sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, limit) 1754 for _, block := range sqlBlocks { 1755 keyword := gulu.Str.SubstringsBetween(block.HPath, search.SearchMarkLeft, search.SearchMarkRight) 1756 if 0 < len(keyword) { 1757 ret = append(ret, keyword...) 1758 } 1759 keyword = gulu.Str.SubstringsBetween(block.Name, search.SearchMarkLeft, search.SearchMarkRight) 1760 if 0 < len(keyword) { 1761 ret = append(ret, keyword...) 1762 } 1763 keyword = gulu.Str.SubstringsBetween(block.Alias, search.SearchMarkLeft, search.SearchMarkRight) 1764 if 0 < len(keyword) { 1765 ret = append(ret, keyword...) 1766 } 1767 keyword = gulu.Str.SubstringsBetween(block.Memo, search.SearchMarkLeft, search.SearchMarkRight) 1768 if 0 < len(keyword) { 1769 ret = append(ret, keyword...) 1770 } 1771 keyword = gulu.Str.SubstringsBetween(block.Tag, search.SearchMarkLeft, search.SearchMarkRight) 1772 if 0 < len(keyword) { 1773 ret = append(ret, keyword...) 1774 } 1775 keyword = gulu.Str.SubstringsBetween(block.IAL, search.SearchMarkLeft, search.SearchMarkRight) 1776 if 0 < len(keyword) { 1777 ret = append(ret, keyword...) 1778 } 1779 keyword = gulu.Str.SubstringsBetween(block.Content, search.SearchMarkLeft, search.SearchMarkRight) 1780 if 0 < len(keyword) { 1781 ret = append(ret, keyword...) 1782 } 1783 } 1784 ret = gulu.Str.RemoveDuplicatedElem(ret) 1785 return 1786} 1787 1788func highlightByRegexp(query, typeFilter, id string) (ret []string) { 1789 fieldFilter := fieldRegexp(query) 1790 stmt := "SELECT * FROM `blocks` WHERE " + fieldFilter + " AND type IN " + typeFilter 1791 stmt += " AND root_id = '" + id + "'" 1792 regex, _ := regexp.Compile(query) 1793 if nil == regex { 1794 return 1795 } 1796 sqlBlocks := sql.SelectBlocksRegex(stmt, regex, Conf.Search.Name, Conf.Search.Alias, Conf.Search.Memo, Conf.Search.IAL, 1, 256) 1797 for _, block := range sqlBlocks { 1798 keyword := gulu.Str.SubstringsBetween(block.HPath, search.SearchMarkLeft, search.SearchMarkRight) 1799 if 0 < len(keyword) { 1800 ret = append(ret, keyword...) 1801 } 1802 keyword = gulu.Str.SubstringsBetween(block.Name, search.SearchMarkLeft, search.SearchMarkRight) 1803 if 0 < len(keyword) { 1804 ret = append(ret, keyword...) 1805 } 1806 keyword = gulu.Str.SubstringsBetween(block.Alias, search.SearchMarkLeft, search.SearchMarkRight) 1807 if 0 < len(keyword) { 1808 ret = append(ret, keyword...) 1809 } 1810 keyword = gulu.Str.SubstringsBetween(block.Memo, search.SearchMarkLeft, search.SearchMarkRight) 1811 if 0 < len(keyword) { 1812 ret = append(ret, keyword...) 1813 } 1814 keyword = gulu.Str.SubstringsBetween(block.Tag, search.SearchMarkLeft, search.SearchMarkRight) 1815 if 0 < len(keyword) { 1816 ret = append(ret, keyword...) 1817 } 1818 keyword = gulu.Str.SubstringsBetween(block.Content, search.SearchMarkLeft, search.SearchMarkRight) 1819 if 0 < len(keyword) { 1820 ret = append(ret, keyword...) 1821 } 1822 } 1823 ret = gulu.Str.RemoveDuplicatedElem(ret) 1824 return 1825} 1826 1827func markSearch(text string, keyword string, beforeLen int) (marked string, score float64) { 1828 if 0 == len(keyword) { 1829 if strings.Contains(text, search.SearchMarkLeft) { // 使用 FTS snippet() 处理过高亮片段,这里简单替换后就返回 1830 marked = util.EscapeHTML(text) 1831 marked = strings.ReplaceAll(marked, search.SearchMarkLeft, "<mark>") 1832 marked = strings.ReplaceAll(marked, search.SearchMarkRight, "</mark>") 1833 return 1834 } 1835 1836 keywords := gulu.Str.SubstringsBetween(text, search.SearchMarkLeft, search.SearchMarkRight) 1837 keywords = gulu.Str.RemoveDuplicatedElem(keywords) 1838 keyword = strings.Join(keywords, search.TermSep) 1839 marked = strings.ReplaceAll(text, search.SearchMarkLeft, "") 1840 marked = strings.ReplaceAll(marked, search.SearchMarkRight, "") 1841 _, marked = search.MarkText(marked, keyword, beforeLen, Conf.Search.CaseSensitive) 1842 marked = util.EscapeHTML(marked) 1843 return 1844 } 1845 1846 pos, marked := search.MarkText(text, keyword, beforeLen, Conf.Search.CaseSensitive) 1847 if -1 < pos { 1848 if 0 == pos { 1849 score = 1 1850 } 1851 score += float64(strings.Count(marked, "<mark>")) 1852 winkler := smetrics.JaroWinkler(text, keyword, 0.7, 4) 1853 score += winkler 1854 } 1855 score = -score // 分越小排序越靠前 1856 return 1857} 1858 1859func fromSQLBlocks(sqlBlocks *[]*sql.Block, terms string, beforeLen int) (ret []*Block) { 1860 for _, sqlBlock := range *sqlBlocks { 1861 ret = append(ret, fromSQLBlock(sqlBlock, terms, beforeLen)) 1862 } 1863 return 1864} 1865 1866func fromSQLBlock(sqlBlock *sql.Block, terms string, beforeLen int) (block *Block) { 1867 if nil == sqlBlock { 1868 return 1869 } 1870 1871 id := sqlBlock.ID 1872 content := sqlBlock.Content 1873 if 1 < strings.Count(content, search.SearchMarkRight) && strings.HasSuffix(content, search.SearchMarkRight+"...") { 1874 // 返回多个关键字命中时需要检查最后一个关键字是否被截断 1875 firstKeyword := gulu.Str.SubStringBetween(content, search.SearchMarkLeft, search.SearchMarkRight) 1876 lastKeyword := gulu.Str.LastSubStringBetween(content, search.SearchMarkLeft, search.SearchMarkRight) 1877 if firstKeyword != lastKeyword { 1878 // 如果第一个关键字和最后一个关键字不相同,说明最后一个关键字被截断了 1879 // 此时需要将 content 中的最后一个关键字替换为完整的关键字 1880 content = strings.TrimSuffix(content, search.SearchMarkLeft+lastKeyword+search.SearchMarkRight+"...") 1881 content += search.SearchMarkLeft + firstKeyword + search.SearchMarkRight + "..." 1882 } 1883 } 1884 1885 content, _ = markSearch(content, terms, beforeLen) 1886 content = maxContent(content, 5120) 1887 tag, _ := markSearch(sqlBlock.Tag, terms, beforeLen) 1888 markdown := maxContent(sqlBlock.Markdown, 5120) 1889 fContent := sqlBlock.FContent 1890 block = &Block{ 1891 Box: sqlBlock.Box, 1892 Path: sqlBlock.Path, 1893 ID: id, 1894 RootID: sqlBlock.RootID, 1895 ParentID: sqlBlock.ParentID, 1896 Alias: sqlBlock.Alias, 1897 Name: sqlBlock.Name, 1898 Memo: sqlBlock.Memo, 1899 Tag: tag, 1900 Content: content, 1901 FContent: fContent, 1902 Markdown: markdown, 1903 Type: treenode.FromAbbrType(sqlBlock.Type), 1904 SubType: sqlBlock.SubType, 1905 Sort: sqlBlock.Sort, 1906 } 1907 if "" != sqlBlock.IAL { 1908 block.IAL = map[string]string{} 1909 ialStr := strings.TrimPrefix(sqlBlock.IAL, "{:") 1910 ialStr = strings.TrimSuffix(ialStr, "}") 1911 ial := parse.Tokens2IAL([]byte(ialStr)) 1912 for _, kv := range ial { 1913 block.IAL[kv[0]] = kv[1] 1914 } 1915 } 1916 1917 hPath, _ := markSearch(sqlBlock.HPath, "", 18) 1918 if !strings.HasPrefix(hPath, "/") { 1919 hPath = "/" + hPath 1920 } 1921 block.HPath = hPath 1922 1923 if "" != block.Name { 1924 block.Name, _ = markSearch(block.Name, terms, 256) 1925 } 1926 if "" != block.Alias { 1927 block.Alias, _ = markSearch(block.Alias, terms, 256) 1928 } 1929 if "" != block.Memo { 1930 block.Memo, _ = markSearch(block.Memo, terms, 256) 1931 } 1932 return 1933} 1934 1935func maxContent(content string, maxLen int) string { 1936 idx := strings.Index(content, "<mark>") 1937 if 128 < maxLen && maxLen <= idx { 1938 head := bytes.Buffer{} 1939 for i := 0; i < 512; i++ { 1940 r, size := utf8.DecodeLastRuneInString(content[:idx]) 1941 head.WriteRune(r) 1942 idx -= size 1943 if 64 < head.Len() { 1944 break 1945 } 1946 } 1947 1948 content = util.Reverse(head.String()) + content[idx:] 1949 } 1950 1951 if maxLen < utf8.RuneCountInString(content) { 1952 return gulu.Str.SubStr(content, maxLen) + "..." 1953 } 1954 return content 1955} 1956 1957func fieldRegexp(regexp string) string { 1958 regexp = strings.ReplaceAll(regexp, "'", "''") // 不需要转义双引号,因为条件都是通过单引号包裹的,只需要转义单引号即可 1959 buf := bytes.Buffer{} 1960 buf.WriteString("(") 1961 buf.WriteString("content REGEXP '") 1962 buf.WriteString(regexp) 1963 buf.WriteString("'") 1964 if Conf.Search.Name { 1965 buf.WriteString(" OR name REGEXP '") 1966 buf.WriteString(regexp) 1967 buf.WriteString("'") 1968 } 1969 if Conf.Search.Alias { 1970 buf.WriteString(" OR alias REGEXP '") 1971 buf.WriteString(regexp) 1972 buf.WriteString("'") 1973 } 1974 if Conf.Search.Memo { 1975 buf.WriteString(" OR memo REGEXP '") 1976 buf.WriteString(regexp) 1977 buf.WriteString("'") 1978 } 1979 if Conf.Search.IAL { 1980 buf.WriteString(" OR ial REGEXP '") 1981 buf.WriteString(regexp) 1982 buf.WriteString("'") 1983 } 1984 buf.WriteString(" OR tag REGEXP '") 1985 buf.WriteString(regexp) 1986 buf.WriteString("')") 1987 return buf.String() 1988} 1989 1990func columnFilter() string { 1991 buf := bytes.Buffer{} 1992 buf.WriteString("{content") 1993 if Conf.Search.Name { 1994 buf.WriteString(" name") 1995 } 1996 if Conf.Search.Alias { 1997 buf.WriteString(" alias") 1998 } 1999 if Conf.Search.Memo { 2000 buf.WriteString(" memo") 2001 } 2002 if Conf.Search.IAL { 2003 buf.WriteString(" ial") 2004 } 2005 buf.WriteString(" tag}") 2006 return buf.String() 2007} 2008 2009func columnConcat() string { 2010 buf := bytes.Buffer{} 2011 buf.WriteString("content") 2012 if Conf.Search.Name { 2013 buf.WriteString("||name") 2014 } 2015 if Conf.Search.Alias { 2016 buf.WriteString("||alias") 2017 } 2018 if Conf.Search.Memo { 2019 buf.WriteString("||memo") 2020 } 2021 if Conf.Search.IAL { 2022 buf.WriteString("||ial") 2023 } 2024 buf.WriteString("||tag") 2025 return buf.String() 2026} 2027 2028func stringQuery(query string) string { 2029 trimmedQuery := strings.TrimSpace(query) 2030 if "" == trimmedQuery { 2031 return "\"" + query + "\"" 2032 } 2033 2034 query = strings.ReplaceAll(query, "\"", "\"\"") 2035 query = strings.ReplaceAll(query, "'", "''") 2036 2037 if strings.Contains(trimmedQuery, " ") { 2038 buf := bytes.Buffer{} 2039 parts := strings.Split(query, " ") 2040 for _, part := range parts { 2041 part = strings.TrimSpace(part) 2042 part = "\"" + part + "\"" 2043 buf.WriteString(part) 2044 buf.WriteString(" ") 2045 } 2046 return strings.TrimSpace(buf.String()) 2047 } 2048 return "\"" + query + "\"" 2049} 2050 2051// markReplaceSpan 用于处理搜索高亮。 2052func markReplaceSpan(n *ast.Node, unlinks *[]*ast.Node, keywords []string, markSpanDataType string, luteEngine *lute.Lute) bool { 2053 if ast.NodeText == n.Type { 2054 text := n.Content() 2055 escapedText := util.EscapeHTML(text) 2056 escapedKeywords := make([]string, len(keywords)) 2057 for i, keyword := range keywords { 2058 escapedKeywords[i] = util.EscapeHTML(keyword) 2059 } 2060 hText := search.EncloseHighlighting(escapedText, escapedKeywords, search.GetMarkSpanStart(markSpanDataType), search.GetMarkSpanEnd(), Conf.Search.CaseSensitive, false) 2061 if hText != escapedText { 2062 text = hText 2063 } 2064 n.Tokens = gulu.Str.ToBytes(text) 2065 if bytes.Contains(n.Tokens, []byte(search.MarkDataType)) { 2066 linkTree := parse.Inline("", n.Tokens, luteEngine.ParseOptions) 2067 var children []*ast.Node 2068 for c := linkTree.Root.FirstChild.FirstChild; nil != c; c = c.Next { 2069 children = append(children, c) 2070 } 2071 for _, c := range children { 2072 n.InsertBefore(c) 2073 } 2074 *unlinks = append(*unlinks, n) 2075 return true 2076 } 2077 } else if ast.NodeTextMark == n.Type { 2078 // 搜索结果高亮支持大部分行级元素 https://github.com/siyuan-note/siyuan/issues/6745 2079 2080 if n.IsTextMarkType("inline-math") || n.IsTextMarkType("inline-memo") { 2081 return false 2082 } 2083 2084 var text string 2085 if n.IsTextMarkType("code") { 2086 // code 在前面的 n. 2087 for i, k := range keywords { 2088 keywords[i] = html.EscapeString(k) 2089 } 2090 text = n.TextMarkTextContent 2091 } else { 2092 text = n.Content() 2093 } 2094 2095 startTag := search.GetMarkSpanStart(markSpanDataType) 2096 text = search.EncloseHighlighting(text, keywords, startTag, search.GetMarkSpanEnd(), Conf.Search.CaseSensitive, false) 2097 if strings.Contains(text, search.MarkDataType) { 2098 dataType := search.GetMarkSpanStart(n.TextMarkType + " " + search.MarkDataType) 2099 text = strings.ReplaceAll(text, startTag, dataType) 2100 tokens := gulu.Str.ToBytes(text) 2101 linkTree := parse.Inline("", tokens, luteEngine.ParseOptions) 2102 var children []*ast.Node 2103 for c := linkTree.Root.FirstChild.FirstChild; nil != c; c = c.Next { 2104 if ast.NodeText == c.Type { 2105 c.Type = ast.NodeTextMark 2106 c.TextMarkType = n.TextMarkType 2107 c.TextMarkTextContent = string(c.Tokens) 2108 if n.IsTextMarkType("a") { 2109 c.TextMarkAHref, c.TextMarkATitle = n.TextMarkAHref, n.TextMarkATitle 2110 } else if treenode.IsBlockRef(n) { 2111 c.TextMarkBlockRefID = n.TextMarkBlockRefID 2112 c.TextMarkBlockRefSubtype = n.TextMarkBlockRefSubtype 2113 } else if treenode.IsFileAnnotationRef(n) { 2114 c.TextMarkFileAnnotationRefID = n.TextMarkFileAnnotationRefID 2115 } 2116 } else if ast.NodeTextMark == c.Type { 2117 if n.IsTextMarkType("a") { 2118 c.TextMarkAHref, c.TextMarkATitle = n.TextMarkAHref, n.TextMarkATitle 2119 } else if treenode.IsBlockRef(n) { 2120 c.TextMarkBlockRefID = n.TextMarkBlockRefID 2121 c.TextMarkBlockRefSubtype = n.TextMarkBlockRefSubtype 2122 } else if treenode.IsFileAnnotationRef(n) { 2123 c.TextMarkFileAnnotationRefID = n.TextMarkFileAnnotationRefID 2124 } 2125 } 2126 2127 children = append(children, c) 2128 if nil != n.Next && ast.NodeKramdownSpanIAL == n.Next.Type { 2129 c.KramdownIAL = n.KramdownIAL 2130 ial := &ast.Node{Type: ast.NodeKramdownSpanIAL, Tokens: n.Next.Tokens} 2131 children = append(children, ial) 2132 } 2133 } 2134 for _, c := range children { 2135 n.InsertBefore(c) 2136 } 2137 *unlinks = append(*unlinks, n) 2138 return true 2139 } 2140 } 2141 return false 2142} 2143 2144// markReplaceSpanWithSplit 用于处理虚拟引用和反链提及高亮。 2145func markReplaceSpanWithSplit(text string, keywords []string, replacementStart, replacementEnd string) (ret string) { 2146 // 虚拟引用和反链提及关键字按最长匹配优先 https://github.com/siyuan-note/siyuan/issues/7465 2147 sort.Slice(keywords, func(i, j int) bool { return len(keywords[i]) > len(keywords[j]) }) 2148 2149 tmp := search.EncloseHighlighting(text, keywords, replacementStart, replacementEnd, Conf.Search.CaseSensitive, true) 2150 parts := strings.Split(tmp, replacementEnd) 2151 buf := bytes.Buffer{} 2152 for i := 0; i < len(parts); i++ { 2153 if i >= len(parts)-1 { 2154 buf.WriteString(parts[i]) 2155 break 2156 } 2157 2158 if nextPart := parts[i+1]; 0 < len(nextPart) && lex.IsASCIILetter(nextPart[0]) { 2159 // 取消已经高亮的部分 2160 part := strings.ReplaceAll(parts[i], replacementStart, "") 2161 buf.WriteString(part) 2162 continue 2163 } 2164 2165 buf.WriteString(parts[i]) 2166 buf.WriteString(replacementEnd) 2167 } 2168 ret = buf.String() 2169 return 2170} 2171 2172var ( 2173 searchIgnoreLastModified int64 2174 searchIgnore []string 2175 searchIgnoreLock = sync.Mutex{} 2176) 2177 2178func getSearchIgnoreLines() (ret []string) { 2179 // Support ignore search results https://github.com/siyuan-note/siyuan/issues/10089 2180 2181 now := time.Now().UnixMilli() 2182 if now-searchIgnoreLastModified < 30*1000 { 2183 return searchIgnore 2184 } 2185 2186 searchIgnoreLock.Lock() 2187 defer searchIgnoreLock.Unlock() 2188 2189 searchIgnoreLastModified = now 2190 2191 searchIgnorePath := filepath.Join(util.DataDir, ".siyuan", "searchignore") 2192 err := os.MkdirAll(filepath.Dir(searchIgnorePath), 0755) 2193 if err != nil { 2194 return 2195 } 2196 if !gulu.File.IsExist(searchIgnorePath) { 2197 if err = gulu.File.WriteFileSafer(searchIgnorePath, nil, 0644); err != nil { 2198 logging.LogErrorf("create searchignore [%s] failed: %s", searchIgnorePath, err) 2199 return 2200 } 2201 } 2202 data, err := os.ReadFile(searchIgnorePath) 2203 if err != nil { 2204 logging.LogErrorf("read searchignore [%s] failed: %s", searchIgnorePath, err) 2205 return 2206 } 2207 dataStr := string(data) 2208 dataStr = strings.ReplaceAll(dataStr, "\r\n", "\n") 2209 ret = strings.Split(dataStr, "\n") 2210 2211 ret = gulu.Str.RemoveDuplicatedElem(ret) 2212 if 0 < len(ret) && "" == ret[0] { 2213 ret = ret[1:] 2214 } 2215 searchIgnore = nil 2216 for _, line := range ret { 2217 searchIgnore = append(searchIgnore, line) 2218 } 2219 return 2220} 2221 2222var ( 2223 refSearchIgnoreLastModified int64 2224 refSearchIgnore []string 2225 refSearchIgnoreLock = sync.Mutex{} 2226) 2227 2228func getRefSearchIgnoreLines() (ret []string) { 2229 // Support ignore search results https://github.com/siyuan-note/siyuan/issues/10089 2230 2231 now := time.Now().UnixMilli() 2232 if now-refSearchIgnoreLastModified < 30*1000 { 2233 return refSearchIgnore 2234 } 2235 2236 refSearchIgnoreLock.Lock() 2237 defer refSearchIgnoreLock.Unlock() 2238 2239 refSearchIgnoreLastModified = now 2240 2241 searchIgnorePath := filepath.Join(util.DataDir, ".siyuan", "refsearchignore") 2242 err := os.MkdirAll(filepath.Dir(searchIgnorePath), 0755) 2243 if err != nil { 2244 return 2245 } 2246 if !gulu.File.IsExist(searchIgnorePath) { 2247 if err = gulu.File.WriteFileSafer(searchIgnorePath, nil, 0644); err != nil { 2248 logging.LogErrorf("create refsearchignore [%s] failed: %s", searchIgnorePath, err) 2249 return 2250 } 2251 } 2252 data, err := os.ReadFile(searchIgnorePath) 2253 if err != nil { 2254 logging.LogErrorf("read refsearchignore [%s] failed: %s", searchIgnorePath, err) 2255 return 2256 } 2257 dataStr := string(data) 2258 dataStr = strings.ReplaceAll(dataStr, "\r\n", "\n") 2259 ret = strings.Split(dataStr, "\n") 2260 2261 ret = gulu.Str.RemoveDuplicatedElem(ret) 2262 if 0 < len(ret) && "" == ret[0] { 2263 ret = ret[1:] 2264 } 2265 refSearchIgnore = nil 2266 for _, line := range ret { 2267 refSearchIgnore = append(refSearchIgnore, line) 2268 } 2269 return 2270} 2271 2272func filterQueryInvisibleChars(query string) string { 2273 query = strings.ReplaceAll(query, " ", "_@full_width_space@_") 2274 query = strings.ReplaceAll(query, "\t", "_@tab@_") 2275 query = strings.ReplaceAll(query, string(gulu.ZWJ), "__@ZWJ@__") 2276 query = util.RemoveInvalid(query) 2277 query = strings.ReplaceAll(query, "_@full_width_space@_", " ") 2278 query = strings.ReplaceAll(query, "_@tab@_", "\t") 2279 query = strings.ReplaceAll(query, "__@ZWJ@__", string(gulu.ZWJ)) 2280 query = strings.ReplaceAll(query, string(gulu.ZWJ)+"#", "#") 2281 return query 2282} 2283 2284func replaceCaseInsensitive(input, old, new []byte) []byte { 2285 re := regexp.MustCompile("(?i)" + regexp.QuoteMeta(string(old))) 2286 return []byte(re.ReplaceAllString(string(input), string(new))) 2287}