2023-06-24 20:39:55 +08:00
|
|
|
|
// SiYuan - Refactor your thinking
|
2022-05-26 15:18:53 +08:00
|
|
|
|
// Copyright (c) 2020-present, b3log.org
|
|
|
|
|
//
|
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
// (at your option) any later version.
|
|
|
|
|
//
|
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
|
//
|
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
|
package model
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
2022-11-28 16:30:08 +08:00
|
|
|
|
"errors"
|
2022-11-29 23:21:19 +08:00
|
|
|
|
"fmt"
|
2023-06-29 18:14:36 +08:00
|
|
|
|
"math"
|
2023-04-03 11:25:56 +08:00
|
|
|
|
"os"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"path"
|
2023-04-03 11:25:56 +08:00
|
|
|
|
"path/filepath"
|
2022-11-28 11:54:04 +08:00
|
|
|
|
"regexp"
|
2022-11-30 12:11:49 +08:00
|
|
|
|
"sort"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
"unicode/utf8"
|
|
|
|
|
|
|
|
|
|
"github.com/88250/gulu"
|
2022-10-26 09:51:09 +08:00
|
|
|
|
"github.com/88250/lute"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"github.com/88250/lute/ast"
|
2022-10-26 09:51:09 +08:00
|
|
|
|
"github.com/88250/lute/lex"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"github.com/88250/lute/parse"
|
2023-04-21 10:46:55 +08:00
|
|
|
|
"github.com/88250/vitess-sqlparser/sqlparser"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"github.com/jinzhu/copier"
|
2023-04-03 11:25:56 +08:00
|
|
|
|
"github.com/siyuan-note/filelock"
|
2022-07-17 12:22:32 +08:00
|
|
|
|
"github.com/siyuan-note/logging"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/conf"
|
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/search"
|
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/sql"
|
2023-04-03 11:25:56 +08:00
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/task"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/treenode"
|
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/util"
|
|
|
|
|
"github.com/xrash/smetrics"
|
|
|
|
|
)
|
|
|
|
|
|
2022-10-12 10:11:08 +08:00
|
|
|
|
type EmbedBlock struct {
|
2022-10-12 15:18:22 +08:00
|
|
|
|
Block *Block `json:"block"`
|
|
|
|
|
BlockPaths []*BlockPath `json:"blockPaths"`
|
2022-10-12 10:11:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-24 17:37:34 +08:00
|
|
|
|
func UpdateEmbedBlock(id, content string) (err error) {
|
|
|
|
|
bt := treenode.GetBlockTree(id)
|
|
|
|
|
if nil == bt {
|
|
|
|
|
err = ErrBlockNotFound
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if treenode.TypeAbbr(ast.NodeBlockQueryEmbed.String()) != bt.Type {
|
|
|
|
|
err = errors.New("not query embed block")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
embedBlock := &EmbedBlock{
|
|
|
|
|
Block: &Block{
|
|
|
|
|
Markdown: content,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateEmbedBlockContent(id, []*EmbedBlock{embedBlock})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-18 12:03:22 +08:00
|
|
|
|
func GetEmbedBlock(embedBlockID string, includeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) {
|
|
|
|
|
return getEmbedBlock(embedBlockID, includeIDs, headingMode, breadcrumb)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getEmbedBlock(embedBlockID string, includeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) {
|
|
|
|
|
stmt := "SELECT * FROM `blocks` WHERE `id` IN ('" + strings.Join(includeIDs, "','") + "')"
|
|
|
|
|
sqlBlocks := sql.SelectBlocksRawStmtNoParse(stmt, 1024)
|
|
|
|
|
ret = buildEmbedBlock(embedBlockID, []string{}, headingMode, breadcrumb, sqlBlocks)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-18 20:41:20 +08:00
|
|
|
|
func SearchEmbedBlock(embedBlockID, stmt string, excludeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) {
|
|
|
|
|
return searchEmbedBlock(embedBlockID, stmt, excludeIDs, headingMode, breadcrumb)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-10-18 20:41:20 +08:00
|
|
|
|
func searchEmbedBlock(embedBlockID, stmt string, excludeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
sqlBlocks := sql.SelectBlocksRawStmtNoParse(stmt, Conf.Search.Limit)
|
2023-11-18 12:03:22 +08:00
|
|
|
|
ret = buildEmbedBlock(embedBlockID, excludeIDs, headingMode, breadcrumb, sqlBlocks)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func buildEmbedBlock(embedBlockID string, excludeIDs []string, headingMode int, breadcrumb bool, sqlBlocks []*sql.Block) (ret []*EmbedBlock) {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
var tmp []*sql.Block
|
|
|
|
|
for _, b := range sqlBlocks {
|
2023-01-19 01:16:13 +08:00
|
|
|
|
if "query_embed" == b.Type { // 嵌入块不再嵌入
|
|
|
|
|
// 嵌入块支持搜索 https://github.com/siyuan-note/siyuan/issues/7112
|
|
|
|
|
// 这里会导致上面的 limit 限制不准确,导致结果变少,暂时没有解决方案,只能靠用户自己调整 SQL,加上 type != 'query_embed' 的条件
|
|
|
|
|
continue
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if !gulu.Str.Contains(b.ID, excludeIDs) {
|
|
|
|
|
tmp = append(tmp, b)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sqlBlocks = tmp
|
2022-10-12 10:11:08 +08:00
|
|
|
|
|
|
|
|
|
// 缓存最多 128 棵语法树
|
|
|
|
|
trees := map[string]*parse.Tree{}
|
|
|
|
|
count := 0
|
|
|
|
|
for _, sb := range sqlBlocks {
|
|
|
|
|
if nil == trees[sb.RootID] {
|
|
|
|
|
tree, _ := loadTreeByBlockID(sb.RootID)
|
|
|
|
|
if nil == tree {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
trees[sb.RootID] = tree
|
|
|
|
|
count++
|
|
|
|
|
}
|
|
|
|
|
if 127 < count {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
for _, sb := range sqlBlocks {
|
2023-11-18 12:03:22 +08:00
|
|
|
|
block, blockPaths := getEmbeddedBlock(trees, sb, headingMode, breadcrumb)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if nil == block {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2022-10-12 10:11:08 +08:00
|
|
|
|
ret = append(ret, &EmbedBlock{
|
2022-10-12 15:18:22 +08:00
|
|
|
|
Block: block,
|
|
|
|
|
BlockPaths: blockPaths,
|
2022-10-12 10:11:08 +08:00
|
|
|
|
})
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-01-19 20:51:32 +08:00
|
|
|
|
// 嵌入块支持搜索 https://github.com/siyuan-note/siyuan/issues/7112
|
2023-02-11 09:56:39 +08:00
|
|
|
|
task.AppendTaskWithTimeout(task.DatabaseIndexEmbedBlock, 30*time.Second, updateEmbedBlockContent, embedBlockID, ret)
|
2023-01-19 20:51:32 +08:00
|
|
|
|
|
2022-10-12 11:44:04 +08:00
|
|
|
|
// 添加笔记本名称
|
|
|
|
|
var boxIDs []string
|
|
|
|
|
for _, embedBlock := range ret {
|
|
|
|
|
boxIDs = append(boxIDs, embedBlock.Block.Box)
|
|
|
|
|
}
|
|
|
|
|
boxIDs = gulu.Str.RemoveDuplicatedElem(boxIDs)
|
|
|
|
|
boxNames := Conf.BoxNames(boxIDs)
|
|
|
|
|
for _, embedBlock := range ret {
|
|
|
|
|
name := boxNames[embedBlock.Block.Box]
|
|
|
|
|
embedBlock.Block.HPath = name + embedBlock.Block.HPath
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if 1 > len(ret) {
|
2022-10-12 10:11:08 +08:00
|
|
|
|
ret = []*EmbedBlock{}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-22 09:27:40 +08:00
|
|
|
|
func SearchRefBlock(id, rootID, keyword string, beforeLen int, isSquareBrackets bool) (ret []*Block, newDoc bool) {
|
2023-02-05 18:32:22 +08:00
|
|
|
|
cachedTrees := map[string]*parse.Tree{}
|
|
|
|
|
|
2023-04-22 09:27:40 +08:00
|
|
|
|
onlyDoc := false
|
|
|
|
|
if isSquareBrackets {
|
|
|
|
|
onlyDoc = Conf.Editor.OnlySearchForDoc
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if "" == keyword {
|
|
|
|
|
// 查询为空时默认的块引排序规则按最近使用优先 https://github.com/siyuan-note/siyuan/issues/3218
|
2023-04-22 09:27:40 +08:00
|
|
|
|
refs := sql.QueryRefsRecent(onlyDoc)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
for _, ref := range refs {
|
2023-02-06 08:54:08 +08:00
|
|
|
|
tree := cachedTrees[ref.DefBlockRootID]
|
2023-02-05 18:32:22 +08:00
|
|
|
|
if nil == tree {
|
2023-02-06 08:54:08 +08:00
|
|
|
|
tree, _ = loadTreeByBlockID(ref.DefBlockRootID)
|
2023-02-05 18:32:22 +08:00
|
|
|
|
}
|
|
|
|
|
if nil == tree {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
continue
|
|
|
|
|
}
|
2023-02-05 18:32:22 +08:00
|
|
|
|
cachedTrees[ref.RootID] = tree
|
|
|
|
|
|
|
|
|
|
node := treenode.GetNodeInTree(tree, ref.DefBlockID)
|
|
|
|
|
if nil == node {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sqlBlock := sql.BuildBlockFromNode(node, tree)
|
|
|
|
|
if nil == sqlBlock {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
block := fromSQLBlock(sqlBlock, "", 0)
|
|
|
|
|
block.RefText = getNodeRefText(node)
|
2022-11-13 21:07:45 +08:00
|
|
|
|
block.RefText = maxContent(block.RefText, Conf.Editor.BlockRefDynamicAnchorTextMaxLen)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
ret = append(ret, block)
|
|
|
|
|
}
|
|
|
|
|
if 1 > len(ret) {
|
|
|
|
|
ret = []*Block{}
|
|
|
|
|
}
|
2023-10-09 17:30:19 +08:00
|
|
|
|
|
|
|
|
|
// 在 hPath 中加入笔记本名 Show notebooks in hpath of block ref search list results https://github.com/siyuan-note/siyuan/issues/9378
|
|
|
|
|
prependNotebookNameInHPath(ret)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-22 09:27:40 +08:00
|
|
|
|
ret = fullTextSearchRefBlock(keyword, beforeLen, onlyDoc)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
tmp := ret[:0]
|
|
|
|
|
for _, b := range ret {
|
2023-02-05 18:32:22 +08:00
|
|
|
|
tree := cachedTrees[b.RootID]
|
|
|
|
|
if nil == tree {
|
|
|
|
|
tree, _ = loadTreeByBlockID(b.RootID)
|
|
|
|
|
}
|
|
|
|
|
if nil == tree {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
cachedTrees[b.RootID] = tree
|
2023-02-05 18:38:25 +08:00
|
|
|
|
b.RefText = getBlockRefText(b.ID, tree)
|
2023-02-05 18:32:22 +08:00
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
hitFirstChildID := false
|
|
|
|
|
if b.IsContainerBlock() {
|
|
|
|
|
// `((` 引用候选中排除当前块的父块 https://github.com/siyuan-note/siyuan/issues/4538
|
2023-02-05 18:32:22 +08:00
|
|
|
|
tree := cachedTrees[b.RootID]
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if nil == tree {
|
|
|
|
|
tree, _ = loadTreeByBlockID(b.RootID)
|
2023-02-05 18:32:22 +08:00
|
|
|
|
cachedTrees[b.RootID] = tree
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
if nil != tree {
|
|
|
|
|
bNode := treenode.GetNodeInTree(tree, b.ID)
|
|
|
|
|
if fc := treenode.FirstLeafBlock(bNode); nil != fc && fc.ID == id {
|
|
|
|
|
hitFirstChildID = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if b.ID != id && !hitFirstChildID && b.ID != rootID {
|
|
|
|
|
tmp = append(tmp, b)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ret = tmp
|
|
|
|
|
|
|
|
|
|
if "" != keyword {
|
|
|
|
|
if block := treenode.GetBlockTree(id); nil != block {
|
|
|
|
|
p := path.Join(block.HPath, keyword)
|
|
|
|
|
newDoc = nil == treenode.GetBlockTreeRootByHPath(block.BoxID, p)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-10-09 17:30:19 +08:00
|
|
|
|
|
|
|
|
|
// 在 hPath 中加入笔记本名 Show notebooks in hpath of block ref search list results https://github.com/siyuan-note/siyuan/issues/9378
|
|
|
|
|
prependNotebookNameInHPath(ret)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-09 17:30:19 +08:00
|
|
|
|
func prependNotebookNameInHPath(blocks []*Block) {
|
|
|
|
|
var boxIDs []string
|
|
|
|
|
for _, b := range blocks {
|
|
|
|
|
boxIDs = append(boxIDs, b.Box)
|
|
|
|
|
}
|
|
|
|
|
boxIDs = gulu.Str.RemoveDuplicatedElem(boxIDs)
|
|
|
|
|
boxNames := Conf.BoxNames(boxIDs)
|
|
|
|
|
for _, b := range blocks {
|
|
|
|
|
name := boxNames[b.Box]
|
2023-10-11 09:04:39 +08:00
|
|
|
|
b.HPath = util.EscapeHTML(name) + b.HPath
|
2023-10-09 17:30:19 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-29 18:06:04 +08:00
|
|
|
|
func FindReplace(keyword, replacement string, ids []string, paths, boxes []string, types map[string]bool, method, orderBy, groupBy int) (err error) {
|
2022-11-28 11:54:04 +08:00
|
|
|
|
// method:0:文本,1:查询语法,2:SQL,3:正则表达式
|
|
|
|
|
if 1 == method || 2 == method {
|
2022-11-28 16:30:08 +08:00
|
|
|
|
err = errors.New(Conf.Language(132))
|
2022-11-28 11:54:04 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-20 10:45:09 +08:00
|
|
|
|
// No longer trim spaces for the keyword and replacement https://github.com/siyuan-note/siyuan/issues/9229
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if keyword == replacement {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 09:17:05 +08:00
|
|
|
|
r, _ := regexp.Compile(keyword)
|
|
|
|
|
escapedKey := util.EscapeHTML(keyword)
|
|
|
|
|
escapedR, _ := regexp.Compile(escapedKey)
|
2022-06-23 19:35:59 +08:00
|
|
|
|
ids = gulu.Str.RemoveDuplicatedElem(ids)
|
2022-06-13 19:59:14 +08:00
|
|
|
|
var renameRoots []*ast.Node
|
|
|
|
|
renameRootTitles := map[string]string{}
|
2023-04-03 11:25:56 +08:00
|
|
|
|
cachedTrees := map[string]*parse.Tree{}
|
2023-04-03 11:38:18 +08:00
|
|
|
|
|
|
|
|
|
historyDir, err := getHistoryDir(HistoryOpReplace, time.Now())
|
|
|
|
|
if nil != err {
|
|
|
|
|
logging.LogErrorf("get history dir failed: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-29 18:06:04 +08:00
|
|
|
|
if 1 > len(ids) {
|
2023-06-29 18:06:18 +08:00
|
|
|
|
// `Replace All` is no longer affected by pagination https://github.com/siyuan-note/siyuan/issues/8265
|
2023-06-29 18:14:36 +08:00
|
|
|
|
blocks, _, _, _ := FullTextSearchBlock(keyword, boxes, paths, types, method, orderBy, groupBy, 1, math.MaxInt)
|
2023-06-29 18:06:04 +08:00
|
|
|
|
for _, block := range blocks {
|
|
|
|
|
ids = append(ids, block.ID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
for _, id := range ids {
|
2023-04-03 11:25:56 +08:00
|
|
|
|
bt := treenode.GetBlockTree(id)
|
|
|
|
|
if nil == bt {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tree := cachedTrees[bt.RootID]
|
|
|
|
|
if nil != tree {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tree, _ = loadTreeByBlockID(id)
|
|
|
|
|
if nil == tree {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-03 11:38:18 +08:00
|
|
|
|
historyPath := filepath.Join(historyDir, tree.Box, tree.Path)
|
|
|
|
|
if err = os.MkdirAll(filepath.Dir(historyPath), 0755); nil != err {
|
|
|
|
|
logging.LogErrorf("generate history failed: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var data []byte
|
|
|
|
|
if data, err = filelock.ReadFile(filepath.Join(util.DataDir, tree.Box, tree.Path)); err != nil {
|
|
|
|
|
logging.LogErrorf("generate history failed: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err = gulu.File.WriteFileSafer(historyPath, data, 0644); err != nil {
|
|
|
|
|
logging.LogErrorf("generate history failed: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-03 11:25:56 +08:00
|
|
|
|
cachedTrees[bt.RootID] = tree
|
|
|
|
|
}
|
2023-04-03 11:38:18 +08:00
|
|
|
|
indexHistoryDir(filepath.Base(historyDir), util.NewLute())
|
2023-04-03 11:25:56 +08:00
|
|
|
|
|
2023-06-29 18:14:36 +08:00
|
|
|
|
for i, id := range ids {
|
2023-04-03 11:25:56 +08:00
|
|
|
|
bt := treenode.GetBlockTree(id)
|
|
|
|
|
if nil == bt {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tree := cachedTrees[bt.RootID]
|
|
|
|
|
if nil == tree {
|
|
|
|
|
continue
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
node := treenode.GetNodeInTree(tree, id)
|
|
|
|
|
if nil == node {
|
2023-04-03 11:25:56 +08:00
|
|
|
|
continue
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-29 21:17:37 +08:00
|
|
|
|
if ast.NodeDocument == node.Type {
|
|
|
|
|
title := node.IALAttr("title")
|
|
|
|
|
if 0 == method {
|
|
|
|
|
if strings.Contains(title, keyword) {
|
|
|
|
|
renameRootTitles[node.ID] = strings.ReplaceAll(title, keyword, replacement)
|
|
|
|
|
renameRoots = append(renameRoots, node)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
2023-06-29 21:17:37 +08:00
|
|
|
|
} else if 3 == method {
|
|
|
|
|
if nil != r && r.MatchString(title) {
|
|
|
|
|
renameRootTitles[node.ID] = r.ReplaceAllString(title, replacement)
|
|
|
|
|
renameRoots = append(renameRoots, node)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
|
|
|
if !entering {
|
|
|
|
|
return ast.WalkContinue
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
2023-06-29 21:17:37 +08:00
|
|
|
|
|
|
|
|
|
switch n.Type {
|
2023-10-28 22:46:28 +08:00
|
|
|
|
case ast.NodeText, ast.NodeLinkDest, ast.NodeLinkText, ast.NodeLinkTitle, ast.NodeCodeBlockCode, ast.NodeMathBlockContent, ast.NodeHTMLBlock:
|
2022-11-28 11:54:04 +08:00
|
|
|
|
if 0 == method {
|
2023-06-29 21:17:37 +08:00
|
|
|
|
if bytes.Contains(n.Tokens, []byte(keyword)) {
|
|
|
|
|
n.Tokens = bytes.ReplaceAll(n.Tokens, []byte(keyword), []byte(replacement))
|
2022-11-28 11:54:04 +08:00
|
|
|
|
}
|
|
|
|
|
} else if 3 == method {
|
2023-06-29 21:17:37 +08:00
|
|
|
|
if nil != r && r.MatchString(string(n.Tokens)) {
|
|
|
|
|
n.Tokens = []byte(r.ReplaceAllString(string(n.Tokens), replacement))
|
2022-11-28 11:54:04 +08:00
|
|
|
|
}
|
2022-11-04 15:44:39 +08:00
|
|
|
|
}
|
2023-06-29 21:17:37 +08:00
|
|
|
|
case ast.NodeTextMark:
|
|
|
|
|
if n.IsTextMarkType("code") {
|
|
|
|
|
if 0 == method {
|
|
|
|
|
if strings.Contains(n.TextMarkTextContent, escapedKey) {
|
|
|
|
|
n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, escapedKey, replacement)
|
|
|
|
|
}
|
|
|
|
|
} else if 3 == method {
|
|
|
|
|
if nil != escapedR && escapedR.MatchString(n.TextMarkTextContent) {
|
|
|
|
|
n.TextMarkTextContent = escapedR.ReplaceAllString(n.TextMarkTextContent, replacement)
|
|
|
|
|
}
|
2022-11-28 11:54:04 +08:00
|
|
|
|
}
|
2023-06-29 21:17:37 +08:00
|
|
|
|
} else {
|
|
|
|
|
if 0 == method {
|
|
|
|
|
if bytes.Contains(n.Tokens, []byte(keyword)) {
|
|
|
|
|
n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, keyword, replacement)
|
|
|
|
|
}
|
|
|
|
|
} else if 3 == method {
|
|
|
|
|
if nil != r && r.MatchString(n.TextMarkTextContent) {
|
|
|
|
|
n.TextMarkTextContent = r.ReplaceAllString(n.TextMarkTextContent, replacement)
|
|
|
|
|
}
|
2022-11-28 11:54:04 +08:00
|
|
|
|
}
|
2022-11-04 15:44:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-29 21:17:37 +08:00
|
|
|
|
if 0 == method {
|
|
|
|
|
if strings.Contains(n.TextMarkTextContent, keyword) {
|
|
|
|
|
n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, keyword, replacement)
|
2022-11-28 11:54:04 +08:00
|
|
|
|
}
|
2023-06-29 21:17:37 +08:00
|
|
|
|
if strings.Contains(n.TextMarkInlineMathContent, keyword) {
|
|
|
|
|
n.TextMarkInlineMathContent = strings.ReplaceAll(n.TextMarkInlineMathContent, keyword, replacement)
|
2022-11-28 11:54:04 +08:00
|
|
|
|
}
|
2023-06-29 21:17:37 +08:00
|
|
|
|
if strings.Contains(n.TextMarkInlineMemoContent, keyword) {
|
|
|
|
|
n.TextMarkInlineMemoContent = strings.ReplaceAll(n.TextMarkInlineMemoContent, keyword, replacement)
|
2022-11-28 11:54:04 +08:00
|
|
|
|
}
|
2023-06-29 21:17:37 +08:00
|
|
|
|
if strings.Contains(n.TextMarkATitle, keyword) {
|
|
|
|
|
n.TextMarkATitle = strings.ReplaceAll(n.TextMarkATitle, keyword, replacement)
|
2022-11-28 11:54:04 +08:00
|
|
|
|
}
|
2023-06-29 21:17:37 +08:00
|
|
|
|
if strings.Contains(n.TextMarkAHref, keyword) {
|
|
|
|
|
n.TextMarkAHref = strings.ReplaceAll(n.TextMarkAHref, keyword, replacement)
|
|
|
|
|
}
|
|
|
|
|
} else if 3 == method {
|
|
|
|
|
if nil != r {
|
|
|
|
|
if r.MatchString(n.TextMarkTextContent) {
|
|
|
|
|
n.TextMarkTextContent = r.ReplaceAllString(n.TextMarkTextContent, replacement)
|
|
|
|
|
}
|
|
|
|
|
if r.MatchString(n.TextMarkInlineMathContent) {
|
|
|
|
|
n.TextMarkInlineMathContent = r.ReplaceAllString(n.TextMarkInlineMathContent, replacement)
|
|
|
|
|
}
|
|
|
|
|
if r.MatchString(n.TextMarkInlineMemoContent) {
|
|
|
|
|
n.TextMarkInlineMemoContent = r.ReplaceAllString(n.TextMarkInlineMemoContent, replacement)
|
|
|
|
|
}
|
|
|
|
|
if r.MatchString(n.TextMarkATitle) {
|
|
|
|
|
n.TextMarkATitle = r.ReplaceAllString(n.TextMarkATitle, replacement)
|
|
|
|
|
}
|
|
|
|
|
if r.MatchString(n.TextMarkAHref) {
|
|
|
|
|
n.TextMarkAHref = r.ReplaceAllString(n.TextMarkAHref, replacement)
|
|
|
|
|
}
|
2022-11-28 11:54:04 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2022-09-18 20:07:38 +08:00
|
|
|
|
}
|
2023-06-29 21:17:37 +08:00
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
})
|
2022-05-26 15:18:53 +08:00
|
|
|
|
|
2023-06-29 21:17:37 +08:00
|
|
|
|
if err = writeJSONQueue(tree); nil != err {
|
|
|
|
|
return
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
2023-06-29 18:14:36 +08:00
|
|
|
|
|
2023-06-29 18:44:45 +08:00
|
|
|
|
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(206), i+1, len(ids)))
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-29 18:44:45 +08:00
|
|
|
|
for i, renameRoot := range renameRoots {
|
2022-06-13 19:59:14 +08:00
|
|
|
|
newTitle := renameRootTitles[renameRoot.ID]
|
|
|
|
|
RenameDoc(renameRoot.Box, renameRoot.Path, newTitle)
|
2023-06-29 18:44:45 +08:00
|
|
|
|
|
2023-06-29 21:17:37 +08:00
|
|
|
|
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(207), i+1, len(renameRoots)))
|
2022-06-13 19:59:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
WaitForWritingFiles()
|
2023-07-06 20:22:19 +08:00
|
|
|
|
if 0 < len(ids) {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
go func() {
|
2023-07-06 20:22:19 +08:00
|
|
|
|
time.Sleep(time.Millisecond * 500)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
util.ReloadUI()
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-02 23:38:57 +08:00
|
|
|
|
// FullTextSearchBlock 搜索内容块。
|
|
|
|
|
//
|
2022-12-09 12:08:07 +08:00
|
|
|
|
// method:0:关键字,1:查询语法,2:SQL,3:正则表达式
|
|
|
|
|
// orderBy: 0:按块类型(默认),1:按创建时间升序,2:按创建时间降序,3:按更新时间升序,4:按更新时间降序,5:按内容顺序(仅在按文档分组时),6:按相关度升序,7:按相关度降序
|
2022-12-02 23:38:57 +08:00
|
|
|
|
// groupBy:0:不分组,1:按文档分组
|
2023-06-29 18:14:36 +08:00
|
|
|
|
func FullTextSearchBlock(query string, boxes, paths []string, types map[string]bool, method, orderBy, groupBy, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount, pageCount int) {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
query = strings.TrimSpace(query)
|
2022-11-26 18:12:54 +08:00
|
|
|
|
beforeLen := 36
|
|
|
|
|
var blocks []*Block
|
2023-04-03 10:13:57 +08:00
|
|
|
|
orderByClause := buildOrderBy(method, orderBy)
|
2022-11-28 11:24:31 +08:00
|
|
|
|
switch method {
|
|
|
|
|
case 1: // 查询语法
|
|
|
|
|
filter := buildTypeFilter(types)
|
2022-11-29 23:21:19 +08:00
|
|
|
|
boxFilter := buildBoxesFilter(boxes)
|
|
|
|
|
pathFilter := buildPathsFilter(paths)
|
2023-06-29 18:14:36 +08:00
|
|
|
|
blocks, matchedBlockCount, matchedRootCount = fullTextSearchByQuerySyntax(query, boxFilter, pathFilter, filter, orderByClause, beforeLen, page, pageSize)
|
2022-11-28 11:24:31 +08:00
|
|
|
|
case 2: // SQL
|
2023-06-29 18:14:36 +08:00
|
|
|
|
blocks, matchedBlockCount, matchedRootCount = searchBySQL(query, beforeLen, page, pageSize)
|
2022-11-28 11:24:31 +08:00
|
|
|
|
case 3: // 正则表达式
|
|
|
|
|
typeFilter := buildTypeFilter(types)
|
2022-11-29 23:21:19 +08:00
|
|
|
|
boxFilter := buildBoxesFilter(boxes)
|
|
|
|
|
pathFilter := buildPathsFilter(paths)
|
2023-06-29 18:14:36 +08:00
|
|
|
|
blocks, matchedBlockCount, matchedRootCount = fullTextSearchByRegexp(query, boxFilter, pathFilter, typeFilter, orderByClause, beforeLen, page, pageSize)
|
2022-12-09 12:08:07 +08:00
|
|
|
|
default: // 关键字
|
2022-11-28 11:24:31 +08:00
|
|
|
|
filter := buildTypeFilter(types)
|
2022-11-29 23:21:19 +08:00
|
|
|
|
boxFilter := buildBoxesFilter(boxes)
|
|
|
|
|
pathFilter := buildPathsFilter(paths)
|
2023-06-29 18:14:36 +08:00
|
|
|
|
blocks, matchedBlockCount, matchedRootCount = fullTextSearchByKeyword(query, boxFilter, pathFilter, filter, orderByClause, beforeLen, page, pageSize)
|
2022-11-26 18:12:54 +08:00
|
|
|
|
}
|
2023-04-21 10:03:05 +08:00
|
|
|
|
pageCount = (matchedBlockCount + pageSize - 1) / pageSize
|
2022-11-26 18:12:54 +08:00
|
|
|
|
|
|
|
|
|
switch groupBy {
|
|
|
|
|
case 0: // 不分组
|
|
|
|
|
ret = blocks
|
|
|
|
|
case 1: // 按文档分组
|
|
|
|
|
rootMap := map[string]bool{}
|
|
|
|
|
var rootIDs []string
|
2022-12-02 23:38:57 +08:00
|
|
|
|
contentSorts := map[string]int{}
|
2022-11-26 18:12:54 +08:00
|
|
|
|
for _, b := range blocks {
|
|
|
|
|
if _, ok := rootMap[b.RootID]; !ok {
|
|
|
|
|
rootMap[b.RootID] = true
|
|
|
|
|
rootIDs = append(rootIDs, b.RootID)
|
2022-11-30 12:11:49 +08:00
|
|
|
|
tree, _ := loadTreeByBlockID(b.RootID)
|
|
|
|
|
if nil == tree {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-02 23:38:57 +08:00
|
|
|
|
if 5 == orderBy { // 按内容顺序(仅在按文档分组时)
|
|
|
|
|
sort := 0
|
|
|
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
|
|
|
if !entering || !n.IsBlock() {
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
2022-11-30 12:11:49 +08:00
|
|
|
|
|
2022-12-02 23:38:57 +08:00
|
|
|
|
contentSorts[n.ID] = sort
|
|
|
|
|
sort++
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
})
|
|
|
|
|
}
|
2022-11-26 18:12:54 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2022-11-30 12:11:49 +08:00
|
|
|
|
|
2022-11-26 18:12:54 +08:00
|
|
|
|
sqlRoots := sql.GetBlocks(rootIDs)
|
|
|
|
|
roots := fromSQLBlocks(&sqlRoots, "", beforeLen)
|
|
|
|
|
for _, root := range roots {
|
|
|
|
|
for _, b := range blocks {
|
2022-12-02 23:38:57 +08:00
|
|
|
|
if 5 == orderBy { // 按内容顺序(仅在按文档分组时)
|
|
|
|
|
b.Sort = contentSorts[b.ID]
|
|
|
|
|
}
|
2022-11-26 18:12:54 +08:00
|
|
|
|
if b.RootID == root.ID {
|
|
|
|
|
root.Children = append(root.Children, b)
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-12-02 23:38:57 +08:00
|
|
|
|
|
|
|
|
|
switch orderBy {
|
|
|
|
|
case 1: //按创建时间升序
|
|
|
|
|
sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Created < root.Children[j].Created })
|
|
|
|
|
case 2: // 按创建时间降序
|
|
|
|
|
sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Created > root.Children[j].Created })
|
|
|
|
|
case 3: // 按更新时间升序
|
|
|
|
|
sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Updated < root.Children[j].Updated })
|
|
|
|
|
case 4: // 按更新时间降序
|
|
|
|
|
sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Updated > root.Children[j].Updated })
|
|
|
|
|
case 5: // 按内容顺序(仅在按文档分组时)
|
|
|
|
|
sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Sort < root.Children[j].Sort })
|
|
|
|
|
default: // 按块类型(默认)
|
|
|
|
|
sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Sort < root.Children[j].Sort })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch orderBy {
|
|
|
|
|
case 1: //按创建时间升序
|
|
|
|
|
sort.Slice(roots, func(i, j int) bool { return roots[i].Created < roots[j].Created })
|
|
|
|
|
case 2: // 按创建时间降序
|
|
|
|
|
sort.Slice(roots, func(i, j int) bool { return roots[i].Created > roots[j].Created })
|
|
|
|
|
case 3: // 按更新时间升序
|
|
|
|
|
sort.Slice(roots, func(i, j int) bool { return roots[i].Updated < roots[j].Updated })
|
|
|
|
|
case 4: // 按更新时间降序
|
|
|
|
|
sort.Slice(roots, func(i, j int) bool { return roots[i].Updated > roots[j].Updated })
|
|
|
|
|
case 5: // 按内容顺序(仅在按文档分组时)
|
2022-12-09 12:08:07 +08:00
|
|
|
|
// 都是文档,不需要再次排序
|
|
|
|
|
case 6, 7: // 按相关度
|
|
|
|
|
// 已在 ORDER BY 中处理
|
2022-12-02 23:38:57 +08:00
|
|
|
|
default: // 按块类型(默认)
|
|
|
|
|
// 都是文档,不需要再次排序
|
2022-11-26 18:12:54 +08:00
|
|
|
|
}
|
|
|
|
|
ret = roots
|
|
|
|
|
default:
|
|
|
|
|
ret = blocks
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
2022-11-27 11:10:20 +08:00
|
|
|
|
if 1 > len(ret) {
|
|
|
|
|
ret = []*Block{}
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-29 23:21:19 +08:00
|
|
|
|
func buildBoxesFilter(boxes []string) string {
|
|
|
|
|
if 0 == len(boxes) {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
builder := bytes.Buffer{}
|
|
|
|
|
builder.WriteString(" AND (")
|
|
|
|
|
for i, box := range boxes {
|
|
|
|
|
builder.WriteString(fmt.Sprintf("box = '%s'", box))
|
|
|
|
|
if i < len(boxes)-1 {
|
|
|
|
|
builder.WriteString(" OR ")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
builder.WriteString(")")
|
|
|
|
|
return builder.String()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func buildPathsFilter(paths []string) string {
|
|
|
|
|
if 0 == len(paths) {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
builder := bytes.Buffer{}
|
|
|
|
|
builder.WriteString(" AND (")
|
|
|
|
|
for i, path := range paths {
|
|
|
|
|
builder.WriteString(fmt.Sprintf("path LIKE '%s%%'", path))
|
|
|
|
|
if i < len(paths)-1 {
|
|
|
|
|
builder.WriteString(" OR ")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
builder.WriteString(")")
|
|
|
|
|
return builder.String()
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-03 10:13:57 +08:00
|
|
|
|
func buildOrderBy(method, orderBy int) string {
|
2022-12-02 17:48:52 +08:00
|
|
|
|
switch orderBy {
|
|
|
|
|
case 1:
|
|
|
|
|
return "ORDER BY created ASC"
|
|
|
|
|
case 2:
|
|
|
|
|
return "ORDER BY created DESC"
|
|
|
|
|
case 3:
|
|
|
|
|
return "ORDER BY updated ASC"
|
|
|
|
|
case 4:
|
|
|
|
|
return "ORDER BY updated DESC"
|
2022-12-09 12:08:07 +08:00
|
|
|
|
case 6:
|
2023-04-03 10:13:57 +08:00
|
|
|
|
if 0 != method && 1 != method {
|
|
|
|
|
// 只有关键字搜索和查询语法搜索才支持按相关度升序 https://github.com/siyuan-note/siyuan/issues/7861
|
2023-06-27 00:05:57 +08:00
|
|
|
|
return "ORDER BY sort DESC, updated DESC"
|
2023-04-03 10:13:57 +08:00
|
|
|
|
}
|
2022-12-09 12:08:07 +08:00
|
|
|
|
return "ORDER BY rank DESC" // 默认是按相关度降序,所以按相关度升序要反过来使用 DESC
|
|
|
|
|
case 7:
|
2023-04-03 10:13:57 +08:00
|
|
|
|
if 0 != method && 1 != method {
|
2023-06-27 00:05:57 +08:00
|
|
|
|
return "ORDER BY sort ASC, updated DESC"
|
2023-04-03 10:13:57 +08:00
|
|
|
|
}
|
2022-12-09 12:08:07 +08:00
|
|
|
|
return "ORDER BY rank" // 默认是按相关度降序
|
2022-12-02 17:48:52 +08:00
|
|
|
|
default:
|
2023-06-27 00:03:31 +08:00
|
|
|
|
return "ORDER BY sort ASC, updated DESC" // Improve search default sort https://github.com/siyuan-note/siyuan/issues/8624
|
2022-12-02 17:48:52 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-28 11:24:31 +08:00
|
|
|
|
func buildTypeFilter(types map[string]bool) string {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
s := conf.NewSearch()
|
|
|
|
|
if err := copier.Copy(s, Conf.Search); nil != err {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("copy search conf failed: %s", err)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
if nil != types {
|
|
|
|
|
s.Document = types["document"]
|
|
|
|
|
s.Heading = types["heading"]
|
|
|
|
|
s.List = types["list"]
|
|
|
|
|
s.ListItem = types["listItem"]
|
|
|
|
|
s.CodeBlock = types["codeBlock"]
|
|
|
|
|
s.MathBlock = types["mathBlock"]
|
|
|
|
|
s.Table = types["table"]
|
|
|
|
|
s.Blockquote = types["blockquote"]
|
|
|
|
|
s.SuperBlock = types["superBlock"]
|
|
|
|
|
s.Paragraph = types["paragraph"]
|
|
|
|
|
s.HTMLBlock = types["htmlBlock"]
|
2023-01-19 01:16:13 +08:00
|
|
|
|
s.EmbedBlock = types["embedBlock"]
|
2023-10-05 12:37:34 +08:00
|
|
|
|
s.DatabaseBlock = types["databaseBlock"]
|
2022-05-26 15:18:53 +08:00
|
|
|
|
} else {
|
|
|
|
|
s.Document = Conf.Search.Document
|
|
|
|
|
s.Heading = Conf.Search.Heading
|
|
|
|
|
s.List = Conf.Search.List
|
|
|
|
|
s.ListItem = Conf.Search.ListItem
|
|
|
|
|
s.CodeBlock = Conf.Search.CodeBlock
|
|
|
|
|
s.MathBlock = Conf.Search.MathBlock
|
|
|
|
|
s.Table = Conf.Search.Table
|
|
|
|
|
s.Blockquote = Conf.Search.Blockquote
|
|
|
|
|
s.SuperBlock = Conf.Search.SuperBlock
|
|
|
|
|
s.Paragraph = Conf.Search.Paragraph
|
|
|
|
|
s.HTMLBlock = Conf.Search.HTMLBlock
|
2023-01-19 01:16:13 +08:00
|
|
|
|
s.EmbedBlock = Conf.Search.EmbedBlock
|
2023-10-05 12:37:34 +08:00
|
|
|
|
s.DatabaseBlock = Conf.Search.DatabaseBlock
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
return s.TypeFilter()
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-29 18:14:36 +08:00
|
|
|
|
func searchBySQL(stmt string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
|
2022-06-11 11:32:00 +08:00
|
|
|
|
stmt = gulu.Str.RemoveInvisible(stmt)
|
2023-02-13 11:01:23 +08:00
|
|
|
|
stmt = strings.TrimSpace(stmt)
|
2023-04-21 10:46:55 +08:00
|
|
|
|
blocks := sql.SelectBlocksRawStmt(stmt, page, pageSize)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
ret = fromSQLBlocks(&blocks, "", beforeLen)
|
|
|
|
|
if 1 > len(ret) {
|
|
|
|
|
ret = []*Block{}
|
2022-07-28 01:14:49 +08:00
|
|
|
|
return
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
2022-07-28 01:14:49 +08:00
|
|
|
|
|
|
|
|
|
stmt = strings.ToLower(stmt)
|
2023-02-13 11:01:23 +08:00
|
|
|
|
if strings.HasPrefix(stmt, "select a.* ") { // 多个搜索关键字匹配文档 https://github.com/siyuan-note/siyuan/issues/7350
|
|
|
|
|
stmt = strings.ReplaceAll(stmt, "select a.* ", "select COUNT(a.id) AS `matches`, COUNT(DISTINCT(a.root_id)) AS `docs` ")
|
|
|
|
|
} else {
|
|
|
|
|
stmt = strings.ReplaceAll(stmt, "select * ", "select COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` ")
|
|
|
|
|
}
|
2023-04-21 10:46:55 +08:00
|
|
|
|
stmt = removeLimitClause(stmt)
|
2023-05-04 10:11:29 +08:00
|
|
|
|
result, _ := sql.QueryNoLimit(stmt)
|
2022-07-28 01:14:49 +08:00
|
|
|
|
if 1 > len(ret) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
matchedBlockCount = int(result[0]["matches"].(int64))
|
|
|
|
|
matchedRootCount = int(result[0]["docs"].(int64))
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-21 10:46:55 +08:00
|
|
|
|
func removeLimitClause(stmt string) string {
|
|
|
|
|
parsedStmt, err := sqlparser.Parse(stmt)
|
|
|
|
|
if nil != err {
|
|
|
|
|
return stmt
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch parsedStmt.(type) {
|
|
|
|
|
case *sqlparser.Select:
|
|
|
|
|
slct := parsedStmt.(*sqlparser.Select)
|
|
|
|
|
if nil != slct.Limit {
|
|
|
|
|
slct.Limit = nil
|
|
|
|
|
}
|
|
|
|
|
stmt = sqlparser.String(slct)
|
|
|
|
|
}
|
|
|
|
|
return stmt
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-22 09:27:40 +08:00
|
|
|
|
func fullTextSearchRefBlock(keyword string, beforeLen int, onlyDoc bool) (ret []*Block) {
|
2022-06-11 11:32:00 +08:00
|
|
|
|
keyword = gulu.Str.RemoveInvisible(keyword)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
|
2023-01-17 22:16:14 +08:00
|
|
|
|
if ast.IsNodeIDPattern(keyword) {
|
2023-06-29 18:14:36 +08:00
|
|
|
|
ret, _, _ = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+keyword+"'", 36, 1, 32)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
quotedKeyword := stringQuery(keyword)
|
|
|
|
|
table := "blocks_fts" // 大小写敏感
|
|
|
|
|
if !Conf.Search.CaseSensitive {
|
|
|
|
|
table = "blocks_fts_case_insensitive"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
projections := "id, parent_id, root_id, hash, box, path, " +
|
2022-08-16 10:24:38 +08:00
|
|
|
|
"snippet(" + table + ", 6, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS hpath, " +
|
|
|
|
|
"snippet(" + table + ", 7, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS name, " +
|
|
|
|
|
"snippet(" + table + ", 8, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS alias, " +
|
|
|
|
|
"snippet(" + table + ", 9, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS memo, " +
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"tag, " +
|
2022-08-16 10:24:38 +08:00
|
|
|
|
"snippet(" + table + ", 11, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS content, " +
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"fcontent, markdown, length, type, subtype, ial, sort, created, updated"
|
2023-04-17 22:47:25 +08:00
|
|
|
|
stmt := "SELECT " + projections + " FROM " + table + " WHERE " + table + " MATCH '" + columnFilter() + ":(" + quotedKeyword + ")' AND type"
|
2023-04-22 09:27:40 +08:00
|
|
|
|
if onlyDoc {
|
2023-04-17 22:47:25 +08:00
|
|
|
|
stmt += " = 'd'"
|
|
|
|
|
} else {
|
|
|
|
|
stmt += " IN " + Conf.Search.TypeFilter()
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
orderBy := ` order by case
|
|
|
|
|
when name = '${keyword}' then 10
|
|
|
|
|
when alias = '${keyword}' then 20
|
|
|
|
|
when memo = '${keyword}' then 30
|
|
|
|
|
when content = '${keyword}' and type = 'd' then 40
|
|
|
|
|
when content LIKE '%${keyword}%' and type = 'd' then 41
|
|
|
|
|
when name LIKE '%${keyword}%' then 50
|
|
|
|
|
when alias LIKE '%${keyword}%' then 60
|
|
|
|
|
when content = '${keyword}' and type = 'h' then 70
|
|
|
|
|
when content LIKE '%${keyword}%' and type = 'h' then 71
|
|
|
|
|
when fcontent = '${keyword}' and type = 'i' then 80
|
|
|
|
|
when fcontent LIKE '%${keyword}%' and type = 'i' then 81
|
|
|
|
|
when memo LIKE '%${keyword}%' then 90
|
|
|
|
|
when content LIKE '%${keyword}%' and type != 'i' and type != 'l' then 100
|
|
|
|
|
else 65535 end ASC, sort ASC, length ASC`
|
2023-08-02 20:47:09 +08:00
|
|
|
|
orderBy = strings.ReplaceAll(orderBy, "${keyword}", quotedKeyword)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
stmt += orderBy + " LIMIT " + strconv.Itoa(Conf.Search.Limit)
|
2023-02-15 14:26:50 +08:00
|
|
|
|
blocks := sql.SelectBlocksRawStmtNoParse(stmt, Conf.Search.Limit)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
ret = fromSQLBlocks(&blocks, "", beforeLen)
|
|
|
|
|
if 1 > len(ret) {
|
|
|
|
|
ret = []*Block{}
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-29 18:14:36 +08:00
|
|
|
|
func fullTextSearchByQuerySyntax(query, boxFilter, pathFilter, typeFilter, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
|
2022-07-28 01:14:49 +08:00
|
|
|
|
query = gulu.Str.RemoveInvisible(query)
|
2023-01-17 22:16:14 +08:00
|
|
|
|
if ast.IsNodeIDPattern(query) {
|
2023-06-29 18:14:36 +08:00
|
|
|
|
ret, matchedBlockCount, matchedRootCount = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+query+"'", beforeLen, page, pageSize)
|
2022-07-28 01:14:49 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
2023-06-29 18:14:36 +08:00
|
|
|
|
return fullTextSearchByFTS(query, boxFilter, pathFilter, typeFilter, orderBy, beforeLen, page, pageSize)
|
2022-12-02 17:30:16 +08:00
|
|
|
|
}
|
2022-07-28 01:14:49 +08:00
|
|
|
|
|
2023-06-29 18:14:36 +08:00
|
|
|
|
func fullTextSearchByKeyword(query, boxFilter, pathFilter, typeFilter string, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
|
2022-12-02 17:30:16 +08:00
|
|
|
|
query = gulu.Str.RemoveInvisible(query)
|
2023-01-17 22:16:14 +08:00
|
|
|
|
if ast.IsNodeIDPattern(query) {
|
2023-06-29 18:14:36 +08:00
|
|
|
|
ret, matchedBlockCount, matchedRootCount = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+query+"'", beforeLen, page, pageSize)
|
2022-12-02 17:30:16 +08:00
|
|
|
|
return
|
2022-11-21 10:19:56 +08:00
|
|
|
|
}
|
2022-12-02 17:30:16 +08:00
|
|
|
|
query = stringQuery(query)
|
2023-06-29 18:14:36 +08:00
|
|
|
|
return fullTextSearchByFTS(query, boxFilter, pathFilter, typeFilter, orderBy, beforeLen, page, pageSize)
|
2022-12-02 17:30:16 +08:00
|
|
|
|
}
|
2022-11-21 10:19:56 +08:00
|
|
|
|
|
2023-06-29 18:14:36 +08:00
|
|
|
|
func fullTextSearchByRegexp(exp, boxFilter, pathFilter, typeFilter, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
|
2022-12-02 17:30:16 +08:00
|
|
|
|
exp = gulu.Str.RemoveInvisible(exp)
|
|
|
|
|
|
|
|
|
|
fieldFilter := fieldRegexp(exp)
|
2022-12-31 12:13:18 +08:00
|
|
|
|
stmt := "SELECT * FROM `blocks` WHERE " + fieldFilter + " AND type IN " + typeFilter
|
2022-12-02 17:30:16 +08:00
|
|
|
|
stmt += boxFilter + pathFilter
|
2022-12-02 17:47:23 +08:00
|
|
|
|
stmt += " " + orderBy
|
2023-04-21 09:34:35 +08:00
|
|
|
|
stmt += " LIMIT " + strconv.Itoa(pageSize) + " OFFSET " + strconv.Itoa((page-1)*pageSize)
|
2023-02-15 14:26:50 +08:00
|
|
|
|
blocks := sql.SelectBlocksRawStmtNoParse(stmt, Conf.Search.Limit)
|
2022-12-02 17:30:16 +08:00
|
|
|
|
ret = fromSQLBlocks(&blocks, "", beforeLen)
|
|
|
|
|
if 1 > len(ret) {
|
|
|
|
|
ret = []*Block{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
matchedBlockCount, matchedRootCount = fullTextSearchCountByRegexp(exp, boxFilter, pathFilter, typeFilter)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fullTextSearchCountByRegexp(exp, boxFilter, pathFilter, typeFilter string) (matchedBlockCount, matchedRootCount int) {
|
|
|
|
|
fieldFilter := fieldRegexp(exp)
|
|
|
|
|
stmt := "SELECT COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` FROM `blocks` WHERE " + fieldFilter + " AND type IN " + typeFilter
|
2022-11-29 23:21:19 +08:00
|
|
|
|
stmt += boxFilter + pathFilter
|
2023-05-04 10:11:29 +08:00
|
|
|
|
result, _ := sql.QueryNoLimit(stmt)
|
2022-07-28 01:14:49 +08:00
|
|
|
|
if 1 > len(result) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
matchedBlockCount = int(result[0]["matches"].(int64))
|
|
|
|
|
matchedRootCount = int(result[0]["docs"].(int64))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-29 18:14:36 +08:00
|
|
|
|
func fullTextSearchByFTS(query, boxFilter, pathFilter, typeFilter, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
table := "blocks_fts" // 大小写敏感
|
|
|
|
|
if !Conf.Search.CaseSensitive {
|
|
|
|
|
table = "blocks_fts_case_insensitive"
|
|
|
|
|
}
|
|
|
|
|
projections := "id, parent_id, root_id, hash, box, path, " +
|
2023-09-18 16:34:21 +08:00
|
|
|
|
// Improve the highlight snippet when search result content is too long https://github.com/siyuan-note/siyuan/issues/9215
|
|
|
|
|
"snippet(" + table + ", 6, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 192) AS hpath, " +
|
|
|
|
|
"snippet(" + table + ", 7, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 192) AS name, " +
|
|
|
|
|
"snippet(" + table + ", 8, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 192) AS alias, " +
|
|
|
|
|
"snippet(" + table + ", 9, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 192) AS memo, " +
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"tag, " +
|
2023-09-18 16:34:21 +08:00
|
|
|
|
"snippet(" + table + ", 11, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 192) AS content, " +
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"fcontent, markdown, length, type, subtype, ial, sort, created, updated"
|
2023-02-15 13:05:07 +08:00
|
|
|
|
stmt := "SELECT " + projections + " FROM " + table + " WHERE (`" + table + "` MATCH '" + columnFilter() + ":(" + query + ")'"
|
|
|
|
|
stmt += ") AND type IN " + typeFilter
|
|
|
|
|
stmt += boxFilter + pathFilter
|
2022-12-02 17:47:23 +08:00
|
|
|
|
stmt += " " + orderBy
|
2023-04-21 09:34:35 +08:00
|
|
|
|
stmt += " LIMIT " + strconv.Itoa(pageSize) + " OFFSET " + strconv.Itoa((page-1)*pageSize)
|
2023-04-21 10:46:55 +08:00
|
|
|
|
blocks := sql.SelectBlocksRawStmt(stmt, page, pageSize)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
ret = fromSQLBlocks(&blocks, "", beforeLen)
|
|
|
|
|
if 1 > len(ret) {
|
|
|
|
|
ret = []*Block{}
|
|
|
|
|
}
|
2022-07-28 01:14:49 +08:00
|
|
|
|
|
2022-11-29 23:21:19 +08:00
|
|
|
|
matchedBlockCount, matchedRootCount = fullTextSearchCount(query, boxFilter, pathFilter, typeFilter)
|
2022-11-28 11:24:31 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-19 10:18:25 +08:00
|
|
|
|
func highlightByQuery(query, typeFilter, id string) (ret []string) {
|
|
|
|
|
const limit = 256
|
|
|
|
|
table := "blocks_fts"
|
|
|
|
|
if !Conf.Search.CaseSensitive {
|
|
|
|
|
table = "blocks_fts_case_insensitive"
|
|
|
|
|
}
|
|
|
|
|
projections := "id, parent_id, root_id, hash, box, path, " +
|
|
|
|
|
"highlight(" + table + ", 6, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS hpath, " +
|
|
|
|
|
"highlight(" + table + ", 7, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS name, " +
|
|
|
|
|
"highlight(" + table + ", 8, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS alias, " +
|
|
|
|
|
"highlight(" + table + ", 9, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS memo, " +
|
|
|
|
|
"tag, " +
|
|
|
|
|
"highlight(" + table + ", 11, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS content, " +
|
|
|
|
|
"fcontent, markdown, length, type, subtype, ial, sort, created, updated"
|
|
|
|
|
stmt := "SELECT " + projections + " FROM " + table + " WHERE (`" + table + "` MATCH '" + columnFilter() + ":(" + query + ")'"
|
|
|
|
|
stmt += ") AND type IN " + typeFilter
|
|
|
|
|
stmt += " AND root_id = '" + id + "'"
|
|
|
|
|
stmt += " LIMIT " + strconv.Itoa(limit)
|
|
|
|
|
sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, limit)
|
|
|
|
|
for _, block := range sqlBlocks {
|
|
|
|
|
keyword := gulu.Str.SubstringsBetween(block.Content, search.SearchMarkLeft, search.SearchMarkRight)
|
|
|
|
|
if 0 < len(keyword) {
|
|
|
|
|
ret = append(ret, keyword...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ret = gulu.Str.RemoveDuplicatedElem(ret)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-02 17:30:16 +08:00
|
|
|
|
func fullTextSearchCount(query, boxFilter, pathFilter, typeFilter string) (matchedBlockCount, matchedRootCount int) {
|
|
|
|
|
query = gulu.Str.RemoveInvisible(query)
|
2023-01-17 22:16:14 +08:00
|
|
|
|
if ast.IsNodeIDPattern(query) {
|
2023-05-04 10:11:29 +08:00
|
|
|
|
ret, _ := sql.QueryNoLimit("SELECT COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` FROM `blocks` WHERE `id` = '" + query + "'")
|
2022-12-02 17:30:16 +08:00
|
|
|
|
if 1 > len(ret) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
matchedBlockCount = int(ret[0]["matches"].(int64))
|
|
|
|
|
matchedRootCount = int(ret[0]["docs"].(int64))
|
|
|
|
|
return
|
2022-11-28 11:24:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-12-02 17:30:16 +08:00
|
|
|
|
table := "blocks_fts" // 大小写敏感
|
|
|
|
|
if !Conf.Search.CaseSensitive {
|
|
|
|
|
table = "blocks_fts_case_insensitive"
|
|
|
|
|
}
|
2022-11-28 11:24:31 +08:00
|
|
|
|
|
2023-02-15 13:05:07 +08:00
|
|
|
|
stmt := "SELECT COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` FROM `" + table + "` WHERE (`" + table + "` MATCH '" + columnFilter() + ":(" + query + ")'"
|
|
|
|
|
stmt += ") AND type IN " + typeFilter
|
|
|
|
|
stmt += boxFilter + pathFilter
|
2023-05-04 10:11:29 +08:00
|
|
|
|
result, _ := sql.QueryNoLimit(stmt)
|
2022-11-28 11:24:31 +08:00
|
|
|
|
if 1 > len(result) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
matchedBlockCount = int(result[0]["matches"].(int64))
|
|
|
|
|
matchedRootCount = int(result[0]["docs"].(int64))
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-08 00:39:18 +08:00
|
|
|
|
func markSearch(text string, keyword string, beforeLen int) (marked string, score float64) {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if 0 == len(keyword) {
|
|
|
|
|
marked = text
|
2022-06-08 00:39:18 +08:00
|
|
|
|
|
2022-08-16 10:24:38 +08:00
|
|
|
|
if strings.Contains(marked, search.SearchMarkLeft) { // 使用 FTS snippet() 处理过高亮片段,这里简单替换后就返回
|
2022-12-28 17:07:59 +08:00
|
|
|
|
marked = util.EscapeHTML(text)
|
2022-08-16 10:24:38 +08:00
|
|
|
|
marked = strings.ReplaceAll(marked, search.SearchMarkLeft, "<mark>")
|
|
|
|
|
marked = strings.ReplaceAll(marked, search.SearchMarkRight, "</mark>")
|
2022-06-08 00:39:18 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-16 10:24:38 +08:00
|
|
|
|
keywords := gulu.Str.SubstringsBetween(marked, search.SearchMarkLeft, search.SearchMarkRight)
|
2022-06-23 19:35:59 +08:00
|
|
|
|
keywords = gulu.Str.RemoveDuplicatedElem(keywords)
|
2022-06-08 00:39:18 +08:00
|
|
|
|
keyword = strings.Join(keywords, search.TermSep)
|
2022-08-16 10:24:38 +08:00
|
|
|
|
marked = strings.ReplaceAll(marked, search.SearchMarkLeft, "")
|
|
|
|
|
marked = strings.ReplaceAll(marked, search.SearchMarkRight, "")
|
2022-06-08 00:39:18 +08:00
|
|
|
|
_, marked = search.MarkText(marked, keyword, beforeLen, Conf.Search.CaseSensitive)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-08 00:39:18 +08:00
|
|
|
|
pos, marked := search.MarkText(text, keyword, beforeLen, Conf.Search.CaseSensitive)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if -1 < pos {
|
|
|
|
|
if 0 == pos {
|
|
|
|
|
score = 1
|
|
|
|
|
}
|
|
|
|
|
score += float64(strings.Count(marked, "<mark>"))
|
|
|
|
|
winkler := smetrics.JaroWinkler(text, keyword, 0.7, 4)
|
|
|
|
|
score += winkler
|
|
|
|
|
}
|
|
|
|
|
score = -score // 分越小排序越靠前
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fromSQLBlocks(sqlBlocks *[]*sql.Block, terms string, beforeLen int) (ret []*Block) {
|
|
|
|
|
for _, sqlBlock := range *sqlBlocks {
|
|
|
|
|
ret = append(ret, fromSQLBlock(sqlBlock, terms, beforeLen))
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fromSQLBlock(sqlBlock *sql.Block, terms string, beforeLen int) (block *Block) {
|
|
|
|
|
if nil == sqlBlock {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
id := sqlBlock.ID
|
2023-06-22 16:58:24 +08:00
|
|
|
|
content := util.EscapeHTML(sqlBlock.Content) // Search dialog XSS https://github.com/siyuan-note/siyuan/issues/8525
|
2022-06-08 00:39:18 +08:00
|
|
|
|
content, _ = markSearch(content, terms, beforeLen)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
content = maxContent(content, 5120)
|
2022-08-28 09:00:01 +08:00
|
|
|
|
markdown := maxContent(sqlBlock.Markdown, 5120)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
|
|
|
|
|
block = &Block{
|
2022-08-28 10:43:56 +08:00
|
|
|
|
Box: sqlBlock.Box,
|
2023-06-13 09:34:36 +08:00
|
|
|
|
Path: sqlBlock.Path,
|
2022-08-28 10:43:56 +08:00
|
|
|
|
ID: id,
|
|
|
|
|
RootID: sqlBlock.RootID,
|
|
|
|
|
ParentID: sqlBlock.ParentID,
|
|
|
|
|
Alias: sqlBlock.Alias,
|
|
|
|
|
Name: sqlBlock.Name,
|
|
|
|
|
Memo: sqlBlock.Memo,
|
|
|
|
|
Tag: sqlBlock.Tag,
|
|
|
|
|
Content: content,
|
|
|
|
|
FContent: sqlBlock.FContent,
|
|
|
|
|
Markdown: markdown,
|
|
|
|
|
Type: treenode.FromAbbrType(sqlBlock.Type),
|
|
|
|
|
SubType: sqlBlock.SubType,
|
2022-11-30 12:11:49 +08:00
|
|
|
|
Sort: sqlBlock.Sort,
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
if "" != sqlBlock.IAL {
|
|
|
|
|
block.IAL = map[string]string{}
|
|
|
|
|
ialStr := strings.TrimPrefix(sqlBlock.IAL, "{:")
|
|
|
|
|
ialStr = strings.TrimSuffix(ialStr, "}")
|
|
|
|
|
ial := parse.Tokens2IAL([]byte(ialStr))
|
|
|
|
|
for _, kv := range ial {
|
|
|
|
|
block.IAL[kv[0]] = kv[1]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-08 00:39:18 +08:00
|
|
|
|
hPath, _ := markSearch(sqlBlock.HPath, terms, 18)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if !strings.HasPrefix(hPath, "/") {
|
|
|
|
|
hPath = "/" + hPath
|
|
|
|
|
}
|
|
|
|
|
block.HPath = hPath
|
|
|
|
|
|
|
|
|
|
if "" != block.Name {
|
2022-06-08 00:39:18 +08:00
|
|
|
|
block.Name, _ = markSearch(block.Name, terms, 256)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
if "" != block.Alias {
|
2022-06-08 00:39:18 +08:00
|
|
|
|
block.Alias, _ = markSearch(block.Alias, terms, 256)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
if "" != block.Memo {
|
2022-06-08 00:39:18 +08:00
|
|
|
|
block.Memo, _ = markSearch(block.Memo, terms, 256)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func maxContent(content string, maxLen int) string {
|
2022-10-09 23:55:04 +08:00
|
|
|
|
idx := strings.Index(content, "<mark>")
|
|
|
|
|
if 128 < maxLen && maxLen <= idx {
|
|
|
|
|
head := bytes.Buffer{}
|
|
|
|
|
for i := 0; i < 512; i++ {
|
|
|
|
|
r, size := utf8.DecodeLastRuneInString(content[:idx])
|
|
|
|
|
head.WriteRune(r)
|
|
|
|
|
idx -= size
|
|
|
|
|
if 64 < head.Len() {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
content = util.Reverse(head.String()) + content[idx:]
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if maxLen < utf8.RuneCountInString(content) {
|
|
|
|
|
return gulu.Str.SubStr(content, maxLen) + "..."
|
|
|
|
|
}
|
|
|
|
|
return content
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-28 11:24:31 +08:00
|
|
|
|
func fieldRegexp(regexp string) string {
|
|
|
|
|
buf := bytes.Buffer{}
|
2022-12-31 12:13:18 +08:00
|
|
|
|
buf.WriteString("(")
|
2022-11-28 11:24:31 +08:00
|
|
|
|
buf.WriteString("content REGEXP '")
|
|
|
|
|
buf.WriteString(regexp)
|
2022-11-28 17:53:41 +08:00
|
|
|
|
buf.WriteString("'")
|
2022-11-28 11:24:31 +08:00
|
|
|
|
if Conf.Search.Name {
|
|
|
|
|
buf.WriteString(" OR name REGEXP '")
|
|
|
|
|
buf.WriteString(regexp)
|
2022-11-28 17:53:41 +08:00
|
|
|
|
buf.WriteString("'")
|
2022-11-28 11:24:31 +08:00
|
|
|
|
}
|
|
|
|
|
if Conf.Search.Alias {
|
|
|
|
|
buf.WriteString(" OR alias REGEXP '")
|
|
|
|
|
buf.WriteString(regexp)
|
2022-11-28 17:53:41 +08:00
|
|
|
|
buf.WriteString("'")
|
2022-11-28 11:24:31 +08:00
|
|
|
|
}
|
|
|
|
|
if Conf.Search.Memo {
|
|
|
|
|
buf.WriteString(" OR memo REGEXP '")
|
|
|
|
|
buf.WriteString(regexp)
|
2022-11-28 17:53:41 +08:00
|
|
|
|
buf.WriteString("'")
|
2022-11-28 11:24:31 +08:00
|
|
|
|
}
|
2023-02-15 14:35:44 +08:00
|
|
|
|
if Conf.Search.IAL {
|
2022-11-28 11:24:31 +08:00
|
|
|
|
buf.WriteString(" OR ial REGEXP '")
|
|
|
|
|
buf.WriteString(regexp)
|
2022-11-28 17:53:41 +08:00
|
|
|
|
buf.WriteString("'")
|
2022-11-28 11:24:31 +08:00
|
|
|
|
}
|
|
|
|
|
buf.WriteString(" OR tag REGEXP '")
|
|
|
|
|
buf.WriteString(regexp)
|
2022-12-31 12:13:18 +08:00
|
|
|
|
buf.WriteString("')")
|
2022-11-28 11:24:31 +08:00
|
|
|
|
return buf.String()
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
func columnFilter() string {
|
|
|
|
|
buf := bytes.Buffer{}
|
|
|
|
|
buf.WriteString("{content")
|
|
|
|
|
if Conf.Search.Name {
|
|
|
|
|
buf.WriteString(" name")
|
|
|
|
|
}
|
|
|
|
|
if Conf.Search.Alias {
|
|
|
|
|
buf.WriteString(" alias")
|
|
|
|
|
}
|
|
|
|
|
if Conf.Search.Memo {
|
|
|
|
|
buf.WriteString(" memo")
|
|
|
|
|
}
|
2023-02-15 14:35:44 +08:00
|
|
|
|
if Conf.Search.IAL {
|
2023-02-15 14:26:50 +08:00
|
|
|
|
buf.WriteString(" ial")
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
buf.WriteString(" tag}")
|
|
|
|
|
return buf.String()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stringQuery(query string) string {
|
|
|
|
|
query = strings.ReplaceAll(query, "\"", "\"\"")
|
2022-06-08 10:09:19 +08:00
|
|
|
|
query = strings.ReplaceAll(query, "'", "''")
|
2022-05-26 15:18:53 +08:00
|
|
|
|
|
|
|
|
|
buf := bytes.Buffer{}
|
|
|
|
|
parts := strings.Split(query, " ")
|
|
|
|
|
for _, part := range parts {
|
|
|
|
|
part = strings.TrimSpace(part)
|
|
|
|
|
part = "\"" + part + "\""
|
|
|
|
|
buf.WriteString(part)
|
|
|
|
|
buf.WriteString(" ")
|
|
|
|
|
}
|
|
|
|
|
return strings.TrimSpace(buf.String())
|
|
|
|
|
}
|
2022-10-13 01:00:59 +08:00
|
|
|
|
|
2022-10-26 09:51:09 +08:00
|
|
|
|
// markReplaceSpan 用于处理搜索高亮。
|
2022-11-29 22:42:15 +08:00
|
|
|
|
func markReplaceSpan(n *ast.Node, unlinks *[]*ast.Node, keywords []string, markSpanDataType string, luteEngine *lute.Lute) bool {
|
|
|
|
|
text := n.Content()
|
|
|
|
|
if ast.NodeText == n.Type {
|
2023-12-05 22:47:57 +08:00
|
|
|
|
text = util.EscapeHTML(text)
|
|
|
|
|
escapedKeywords := make([]string, len(keywords))
|
|
|
|
|
for i, keyword := range keywords {
|
|
|
|
|
escapedKeywords[i] = util.EscapeHTML(keyword)
|
|
|
|
|
}
|
|
|
|
|
text = search.EncloseHighlighting(text, escapedKeywords, search.GetMarkSpanStart(markSpanDataType), search.GetMarkSpanEnd(), Conf.Search.CaseSensitive, false)
|
2022-11-29 22:42:15 +08:00
|
|
|
|
n.Tokens = gulu.Str.ToBytes(text)
|
2023-02-16 10:41:02 +08:00
|
|
|
|
if bytes.Contains(n.Tokens, []byte(search.MarkDataType)) {
|
2022-11-29 22:42:15 +08:00
|
|
|
|
linkTree := parse.Inline("", n.Tokens, luteEngine.ParseOptions)
|
|
|
|
|
var children []*ast.Node
|
|
|
|
|
for c := linkTree.Root.FirstChild.FirstChild; nil != c; c = c.Next {
|
|
|
|
|
children = append(children, c)
|
|
|
|
|
}
|
|
|
|
|
for _, c := range children {
|
|
|
|
|
n.InsertBefore(c)
|
|
|
|
|
}
|
|
|
|
|
*unlinks = append(*unlinks, n)
|
|
|
|
|
return true
|
2022-10-20 00:58:54 +08:00
|
|
|
|
}
|
2022-11-29 22:42:15 +08:00
|
|
|
|
} else if ast.NodeTextMark == n.Type {
|
2022-12-28 16:00:40 +08:00
|
|
|
|
// 搜索结果高亮支持大部分行级元素 https://github.com/siyuan-note/siyuan/issues/6745
|
2023-05-19 10:18:25 +08:00
|
|
|
|
|
2022-12-28 16:00:40 +08:00
|
|
|
|
if n.IsTextMarkType("inline-math") || n.IsTextMarkType("inline-memo") {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 10:41:02 +08:00
|
|
|
|
startTag := search.GetMarkSpanStart(markSpanDataType)
|
2023-04-25 11:16:48 +08:00
|
|
|
|
text = search.EncloseHighlighting(text, keywords, startTag, search.GetMarkSpanEnd(), Conf.Search.CaseSensitive, false)
|
2023-02-16 10:41:02 +08:00
|
|
|
|
if strings.Contains(text, search.MarkDataType) {
|
|
|
|
|
dataType := search.GetMarkSpanStart(n.TextMarkType + " " + search.MarkDataType)
|
2022-12-28 16:00:40 +08:00
|
|
|
|
text = strings.ReplaceAll(text, startTag, dataType)
|
|
|
|
|
tokens := gulu.Str.ToBytes(text)
|
|
|
|
|
linkTree := parse.Inline("", tokens, luteEngine.ParseOptions)
|
|
|
|
|
var children []*ast.Node
|
|
|
|
|
for c := linkTree.Root.FirstChild.FirstChild; nil != c; c = c.Next {
|
|
|
|
|
if ast.NodeText == c.Type {
|
|
|
|
|
c.Type = ast.NodeTextMark
|
|
|
|
|
c.TextMarkType = n.TextMarkType
|
|
|
|
|
c.TextMarkTextContent = string(c.Tokens)
|
2023-01-03 22:57:43 +08:00
|
|
|
|
if n.IsTextMarkType("a") {
|
|
|
|
|
c.TextMarkAHref, c.TextMarkATitle = n.TextMarkAHref, n.TextMarkATitle
|
2023-02-10 15:21:20 +08:00
|
|
|
|
} else if treenode.IsBlockRef(n) {
|
2023-01-03 22:57:43 +08:00
|
|
|
|
c.TextMarkBlockRefID = n.TextMarkBlockRefID
|
|
|
|
|
c.TextMarkBlockRefSubtype = n.TextMarkBlockRefSubtype
|
2023-02-10 15:21:20 +08:00
|
|
|
|
} else if treenode.IsFileAnnotationRef(n) {
|
2023-01-03 22:57:43 +08:00
|
|
|
|
c.TextMarkFileAnnotationRefID = n.TextMarkFileAnnotationRefID
|
|
|
|
|
}
|
|
|
|
|
} else if ast.NodeTextMark == c.Type {
|
|
|
|
|
if n.IsTextMarkType("a") {
|
|
|
|
|
c.TextMarkAHref, c.TextMarkATitle = n.TextMarkAHref, n.TextMarkATitle
|
2023-02-10 15:21:20 +08:00
|
|
|
|
} else if treenode.IsBlockRef(n) {
|
2023-01-03 22:57:43 +08:00
|
|
|
|
c.TextMarkBlockRefID = n.TextMarkBlockRefID
|
|
|
|
|
c.TextMarkBlockRefSubtype = n.TextMarkBlockRefSubtype
|
2023-02-10 15:21:20 +08:00
|
|
|
|
} else if treenode.IsFileAnnotationRef(n) {
|
2023-01-03 22:57:43 +08:00
|
|
|
|
c.TextMarkFileAnnotationRefID = n.TextMarkFileAnnotationRefID
|
|
|
|
|
}
|
2022-12-28 16:00:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
children = append(children, c)
|
|
|
|
|
if nil != n.Next && ast.NodeKramdownSpanIAL == n.Next.Type {
|
|
|
|
|
c.KramdownIAL = n.KramdownIAL
|
|
|
|
|
ial := &ast.Node{Type: ast.NodeKramdownSpanIAL, Tokens: n.Next.Tokens}
|
|
|
|
|
children = append(children, ial)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, c := range children {
|
|
|
|
|
n.InsertBefore(c)
|
|
|
|
|
}
|
|
|
|
|
*unlinks = append(*unlinks, n)
|
|
|
|
|
return true
|
|
|
|
|
}
|
2022-10-20 00:58:54 +08:00
|
|
|
|
}
|
2022-10-26 09:51:09 +08:00
|
|
|
|
return false
|
2022-10-20 00:58:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-10-26 09:51:09 +08:00
|
|
|
|
// markReplaceSpanWithSplit 用于处理虚拟引用和反链提及高亮。
|
|
|
|
|
func markReplaceSpanWithSplit(text string, keywords []string, replacementStart, replacementEnd string) (ret string) {
|
2023-02-24 10:04:25 +08:00
|
|
|
|
// 虚拟引用和反链提及关键字按最长匹配优先 https://github.com/siyuan-note/siyuan/issues/7465
|
|
|
|
|
sort.Slice(keywords, func(i, j int) bool { return len(keywords[i]) > len(keywords[j]) })
|
|
|
|
|
|
2023-04-25 11:16:48 +08:00
|
|
|
|
tmp := search.EncloseHighlighting(text, keywords, replacementStart, replacementEnd, Conf.Search.CaseSensitive, true)
|
2022-11-16 10:34:02 +08:00
|
|
|
|
parts := strings.Split(tmp, replacementEnd)
|
|
|
|
|
buf := bytes.Buffer{}
|
|
|
|
|
for i := 0; i < len(parts); i++ {
|
|
|
|
|
if i >= len(parts)-1 {
|
|
|
|
|
buf.WriteString(parts[i])
|
|
|
|
|
break
|
2022-10-13 01:00:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-11-16 10:34:02 +08:00
|
|
|
|
if nextPart := parts[i+1]; 0 < len(nextPart) && lex.IsASCIILetter(nextPart[0]) {
|
|
|
|
|
// 取消已经高亮的部分
|
|
|
|
|
part := strings.ReplaceAll(parts[i], replacementStart, "")
|
|
|
|
|
buf.WriteString(part)
|
|
|
|
|
continue
|
2022-10-14 22:32:13 +08:00
|
|
|
|
}
|
2022-10-17 22:21:57 +08:00
|
|
|
|
|
2022-11-16 10:34:02 +08:00
|
|
|
|
buf.WriteString(parts[i])
|
|
|
|
|
buf.WriteString(replacementEnd)
|
2022-10-17 22:21:57 +08:00
|
|
|
|
}
|
2022-11-16 10:34:02 +08:00
|
|
|
|
ret = buf.String()
|
2022-10-17 22:21:57 +08:00
|
|
|
|
return
|
2022-10-13 01:00:59 +08:00
|
|
|
|
}
|