search.go 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777
  1. // SiYuan - Refactor your thinking
  2. // Copyright (c) 2020-present, b3log.org
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. package model
  17. import (
  18. "bytes"
  19. "errors"
  20. "fmt"
  21. "math"
  22. "os"
  23. "path"
  24. "path/filepath"
  25. "regexp"
  26. "sort"
  27. "strconv"
  28. "strings"
  29. "sync"
  30. "time"
  31. "unicode/utf8"
  32. "github.com/88250/gulu"
  33. "github.com/88250/lute"
  34. "github.com/88250/lute/ast"
  35. "github.com/88250/lute/html"
  36. "github.com/88250/lute/lex"
  37. "github.com/88250/lute/parse"
  38. "github.com/88250/vitess-sqlparser/sqlparser"
  39. "github.com/jinzhu/copier"
  40. "github.com/siyuan-note/filelock"
  41. "github.com/siyuan-note/logging"
  42. "github.com/siyuan-note/siyuan/kernel/conf"
  43. "github.com/siyuan-note/siyuan/kernel/search"
  44. "github.com/siyuan-note/siyuan/kernel/sql"
  45. "github.com/siyuan-note/siyuan/kernel/task"
  46. "github.com/siyuan-note/siyuan/kernel/treenode"
  47. "github.com/siyuan-note/siyuan/kernel/util"
  48. "github.com/xrash/smetrics"
  49. )
  50. func ListInvalidBlockRefs(page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount, pageCount int) {
  51. refBlockMap := map[string][]string{}
  52. blockMap := map[string]bool{}
  53. var invalidBlockIDs []string
  54. notebooks, err := ListNotebooks()
  55. if nil != err {
  56. return
  57. }
  58. luteEngine := util.NewLute()
  59. for _, notebook := range notebooks {
  60. pages := pagedPaths(filepath.Join(util.DataDir, notebook.ID), 32)
  61. for _, paths := range pages {
  62. var trees []*parse.Tree
  63. for _, localPath := range paths {
  64. tree, loadTreeErr := loadTree(localPath, luteEngine)
  65. if nil != loadTreeErr {
  66. continue
  67. }
  68. trees = append(trees, tree)
  69. }
  70. for _, tree := range trees {
  71. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  72. if entering {
  73. if n.IsBlock() {
  74. blockMap[n.ID] = true
  75. return ast.WalkContinue
  76. }
  77. if ast.NodeTextMark == n.Type {
  78. if n.IsTextMarkType("a") {
  79. if strings.HasPrefix(n.TextMarkAHref, "siyuan://blocks/") {
  80. defID := strings.TrimPrefix(n.TextMarkAHref, "siyuan://blocks/")
  81. if strings.Contains(defID, "?") {
  82. defID = strings.Split(defID, "?")[0]
  83. }
  84. refID := treenode.ParentBlock(n).ID
  85. if defIDs := refBlockMap[refID]; 1 > len(defIDs) {
  86. refBlockMap[refID] = []string{defID}
  87. } else {
  88. refBlockMap[refID] = append(defIDs, defID)
  89. }
  90. }
  91. } else if n.IsTextMarkType("block-ref") {
  92. defID := n.TextMarkBlockRefID
  93. refID := treenode.ParentBlock(n).ID
  94. if defIDs := refBlockMap[refID]; 1 > len(defIDs) {
  95. refBlockMap[refID] = []string{defID}
  96. } else {
  97. refBlockMap[refID] = append(defIDs, defID)
  98. }
  99. }
  100. }
  101. }
  102. return ast.WalkContinue
  103. })
  104. }
  105. }
  106. }
  107. invalidDefIDs := map[string]bool{}
  108. for _, refDefIDs := range refBlockMap {
  109. for _, defID := range refDefIDs {
  110. invalidDefIDs[defID] = true
  111. }
  112. }
  113. var toRemoves []string
  114. for defID, _ := range invalidDefIDs {
  115. if _, ok := blockMap[defID]; ok {
  116. toRemoves = append(toRemoves, defID)
  117. }
  118. }
  119. for _, toRemove := range toRemoves {
  120. delete(invalidDefIDs, toRemove)
  121. }
  122. toRemoves = nil
  123. for refID, defIDs := range refBlockMap {
  124. var tmp []string
  125. for _, defID := range defIDs {
  126. if _, ok := invalidDefIDs[defID]; !ok {
  127. tmp = append(tmp, defID)
  128. }
  129. }
  130. for _, toRemove := range tmp {
  131. defIDs = gulu.Str.RemoveElem(defIDs, toRemove)
  132. }
  133. if 1 > len(defIDs) {
  134. toRemoves = append(toRemoves, refID)
  135. }
  136. }
  137. for _, toRemove := range toRemoves {
  138. delete(refBlockMap, toRemove)
  139. }
  140. for refID, _ := range refBlockMap {
  141. invalidBlockIDs = append(invalidBlockIDs, refID)
  142. }
  143. invalidBlockIDs = gulu.Str.RemoveDuplicatedElem(invalidBlockIDs)
  144. sort.Strings(invalidBlockIDs)
  145. allInvalidBlockIDs := invalidBlockIDs
  146. start := (page - 1) * pageSize
  147. end := page * pageSize
  148. if end > len(invalidBlockIDs) {
  149. end = len(invalidBlockIDs)
  150. }
  151. invalidBlockIDs = invalidBlockIDs[start:end]
  152. sqlBlocks := sql.GetBlocks(invalidBlockIDs)
  153. var tmp []*sql.Block
  154. for _, sqlBlock := range sqlBlocks {
  155. if nil != sqlBlock {
  156. tmp = append(tmp, sqlBlock)
  157. }
  158. }
  159. sqlBlocks = tmp
  160. ret = fromSQLBlocks(&sqlBlocks, "", 36)
  161. if 1 > len(ret) {
  162. ret = []*Block{}
  163. }
  164. matchedBlockCount = len(allInvalidBlockIDs)
  165. rootCount := map[string]bool{}
  166. for _, id := range allInvalidBlockIDs {
  167. bt := treenode.GetBlockTree(id)
  168. if nil == bt {
  169. continue
  170. }
  171. rootCount[bt.RootID] = true
  172. }
  173. matchedRootCount = len(rootCount)
  174. pageCount = (matchedBlockCount + pageSize - 1) / pageSize
  175. return
  176. }
  177. type EmbedBlock struct {
  178. Block *Block `json:"block"`
  179. BlockPaths []*BlockPath `json:"blockPaths"`
  180. }
  181. func UpdateEmbedBlock(id, content string) (err error) {
  182. bt := treenode.GetBlockTree(id)
  183. if nil == bt {
  184. err = ErrBlockNotFound
  185. return
  186. }
  187. if treenode.TypeAbbr(ast.NodeBlockQueryEmbed.String()) != bt.Type {
  188. err = errors.New("not query embed block")
  189. return
  190. }
  191. embedBlock := &EmbedBlock{
  192. Block: &Block{
  193. Markdown: content,
  194. },
  195. }
  196. updateEmbedBlockContent(id, []*EmbedBlock{embedBlock})
  197. return
  198. }
  199. func GetEmbedBlock(embedBlockID string, includeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) {
  200. return getEmbedBlock(embedBlockID, includeIDs, headingMode, breadcrumb)
  201. }
  202. func getEmbedBlock(embedBlockID string, includeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) {
  203. stmt := "SELECT * FROM `blocks` WHERE `id` IN ('" + strings.Join(includeIDs, "','") + "')"
  204. sqlBlocks := sql.SelectBlocksRawStmtNoParse(stmt, 1024)
  205. // 根据 includeIDs 的顺序排序 Improve `//!js` query embed block result sorting https://github.com/siyuan-note/siyuan/issues/9977
  206. m := map[string]int{}
  207. for i, id := range includeIDs {
  208. m[id] = i
  209. }
  210. sort.Slice(sqlBlocks, func(i, j int) bool {
  211. return m[sqlBlocks[i].ID] < m[sqlBlocks[j].ID]
  212. })
  213. ret = buildEmbedBlock(embedBlockID, []string{}, headingMode, breadcrumb, sqlBlocks)
  214. return
  215. }
  216. func SearchEmbedBlock(embedBlockID, stmt string, excludeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) {
  217. return searchEmbedBlock(embedBlockID, stmt, excludeIDs, headingMode, breadcrumb)
  218. }
  219. func searchEmbedBlock(embedBlockID, stmt string, excludeIDs []string, headingMode int, breadcrumb bool) (ret []*EmbedBlock) {
  220. sqlBlocks := sql.SelectBlocksRawStmtNoParse(stmt, Conf.Search.Limit)
  221. ret = buildEmbedBlock(embedBlockID, excludeIDs, headingMode, breadcrumb, sqlBlocks)
  222. return
  223. }
  224. func buildEmbedBlock(embedBlockID string, excludeIDs []string, headingMode int, breadcrumb bool, sqlBlocks []*sql.Block) (ret []*EmbedBlock) {
  225. var tmp []*sql.Block
  226. for _, b := range sqlBlocks {
  227. if "query_embed" == b.Type { // 嵌入块不再嵌入
  228. // 嵌入块支持搜索 https://github.com/siyuan-note/siyuan/issues/7112
  229. // 这里会导致上面的 limit 限制不准确,导致结果变少,暂时没有解决方案,只能靠用户自己调整 SQL,加上 type != 'query_embed' 的条件
  230. continue
  231. }
  232. if !gulu.Str.Contains(b.ID, excludeIDs) {
  233. tmp = append(tmp, b)
  234. }
  235. }
  236. sqlBlocks = tmp
  237. // 缓存最多 128 棵语法树
  238. trees := map[string]*parse.Tree{}
  239. count := 0
  240. for _, sb := range sqlBlocks {
  241. if nil == trees[sb.RootID] {
  242. tree, _ := LoadTreeByBlockID(sb.RootID)
  243. if nil == tree {
  244. continue
  245. }
  246. trees[sb.RootID] = tree
  247. count++
  248. }
  249. if 127 < count {
  250. break
  251. }
  252. }
  253. for _, sb := range sqlBlocks {
  254. block, blockPaths := getEmbeddedBlock(trees, sb, headingMode, breadcrumb)
  255. if nil == block {
  256. continue
  257. }
  258. ret = append(ret, &EmbedBlock{
  259. Block: block,
  260. BlockPaths: blockPaths,
  261. })
  262. }
  263. // 嵌入块支持搜索 https://github.com/siyuan-note/siyuan/issues/7112
  264. task.AppendTaskWithTimeout(task.DatabaseIndexEmbedBlock, 30*time.Second, updateEmbedBlockContent, embedBlockID, ret)
  265. // 添加笔记本名称
  266. var boxIDs []string
  267. for _, embedBlock := range ret {
  268. boxIDs = append(boxIDs, embedBlock.Block.Box)
  269. }
  270. boxIDs = gulu.Str.RemoveDuplicatedElem(boxIDs)
  271. boxNames := Conf.BoxNames(boxIDs)
  272. for _, embedBlock := range ret {
  273. name := boxNames[embedBlock.Block.Box]
  274. embedBlock.Block.HPath = name + embedBlock.Block.HPath
  275. }
  276. if 1 > len(ret) {
  277. ret = []*EmbedBlock{}
  278. }
  279. return
  280. }
  281. func SearchRefBlock(id, rootID, keyword string, beforeLen int, isSquareBrackets, isDatabase bool) (ret []*Block, newDoc bool) {
  282. cachedTrees := map[string]*parse.Tree{}
  283. onlyDoc := false
  284. if isSquareBrackets {
  285. onlyDoc = Conf.Editor.OnlySearchForDoc
  286. }
  287. if "" == keyword {
  288. // 查询为空时默认的块引排序规则按最近使用优先 https://github.com/siyuan-note/siyuan/issues/3218
  289. refs := sql.QueryRefsRecent(onlyDoc)
  290. for _, ref := range refs {
  291. tree := cachedTrees[ref.DefBlockRootID]
  292. if nil == tree {
  293. tree, _ = LoadTreeByBlockID(ref.DefBlockRootID)
  294. }
  295. if nil == tree {
  296. continue
  297. }
  298. cachedTrees[ref.RootID] = tree
  299. node := treenode.GetNodeInTree(tree, ref.DefBlockID)
  300. if nil == node {
  301. continue
  302. }
  303. sqlBlock := sql.BuildBlockFromNode(node, tree)
  304. if nil == sqlBlock {
  305. return
  306. }
  307. block := fromSQLBlock(sqlBlock, "", 0)
  308. block.RefText = getNodeRefText(node)
  309. block.RefText = maxContent(block.RefText, Conf.Editor.BlockRefDynamicAnchorTextMaxLen)
  310. ret = append(ret, block)
  311. }
  312. if 1 > len(ret) {
  313. ret = []*Block{}
  314. }
  315. // 在 hPath 中加入笔记本名 Show notebooks in hpath of block ref search list results https://github.com/siyuan-note/siyuan/issues/9378
  316. prependNotebookNameInHPath(ret)
  317. return
  318. }
  319. ret = fullTextSearchRefBlock(keyword, beforeLen, onlyDoc)
  320. tmp := ret[:0]
  321. for _, b := range ret {
  322. tree := cachedTrees[b.RootID]
  323. if nil == tree {
  324. tree, _ = LoadTreeByBlockID(b.RootID)
  325. }
  326. if nil == tree {
  327. continue
  328. }
  329. cachedTrees[b.RootID] = tree
  330. b.RefText = getBlockRefText(b.ID, tree)
  331. hitFirstChildID := false
  332. if b.IsContainerBlock() && "NodeDocument" != b.Type {
  333. // `((` 引用候选中排除当前块的父块 https://github.com/siyuan-note/siyuan/issues/4538
  334. tree := cachedTrees[b.RootID]
  335. if nil == tree {
  336. tree, _ = LoadTreeByBlockID(b.RootID)
  337. cachedTrees[b.RootID] = tree
  338. }
  339. if nil != tree {
  340. bNode := treenode.GetNodeInTree(tree, b.ID)
  341. if fc := treenode.FirstLeafBlock(bNode); nil != fc && fc.ID == id {
  342. hitFirstChildID = true
  343. }
  344. }
  345. }
  346. if "NodeAttributeView" == b.Type {
  347. // 数据库块可以添加到自身数据库块中,当前文档也可以添加到自身数据库块中
  348. tmp = append(tmp, b)
  349. } else {
  350. // 排除自身块、父块和根块
  351. if b.ID != id && !hitFirstChildID && b.ID != rootID {
  352. tmp = append(tmp, b)
  353. }
  354. }
  355. }
  356. ret = tmp
  357. if !isDatabase {
  358. // 如果非数据库中搜索块引,则不允许新建重名文档
  359. // 如果是数据库中搜索绑定块,则允许新建重名文档 https://github.com/siyuan-note/siyuan/issues/11713
  360. if block := treenode.GetBlockTree(id); nil != block {
  361. p := path.Join(block.HPath, keyword)
  362. newDoc = nil == treenode.GetBlockTreeRootByHPath(block.BoxID, p)
  363. }
  364. }
  365. // 在 hPath 中加入笔记本名 Show notebooks in hpath of block ref search list results https://github.com/siyuan-note/siyuan/issues/9378
  366. prependNotebookNameInHPath(ret)
  367. return
  368. }
  369. func prependNotebookNameInHPath(blocks []*Block) {
  370. var boxIDs []string
  371. for _, b := range blocks {
  372. boxIDs = append(boxIDs, b.Box)
  373. }
  374. boxIDs = gulu.Str.RemoveDuplicatedElem(boxIDs)
  375. boxNames := Conf.BoxNames(boxIDs)
  376. for _, b := range blocks {
  377. name := boxNames[b.Box]
  378. b.HPath = util.EscapeHTML(name) + b.HPath
  379. }
  380. }
  381. func FindReplace(keyword, replacement string, replaceTypes map[string]bool, ids []string, paths, boxes []string, types map[string]bool, method, orderBy, groupBy int) (err error) {
  382. // method:0:文本,1:查询语法,2:SQL,3:正则表达式
  383. if 1 == method || 2 == method {
  384. err = errors.New(Conf.Language(132))
  385. return
  386. }
  387. if 0 != groupBy {
  388. // 按文档分组后不支持替换 Need to be reminded that replacement operations are not supported after grouping by doc https://github.com/siyuan-note/siyuan/issues/10161
  389. // 因为分组条件传入以后搜索只能命中文档块,会导致 全部替换 失效
  390. err = errors.New(Conf.Language(221))
  391. return
  392. }
  393. // No longer trim spaces for the keyword and replacement https://github.com/siyuan-note/siyuan/issues/9229
  394. if keyword == replacement {
  395. return
  396. }
  397. r, _ := regexp.Compile(keyword)
  398. escapedKey := util.EscapeHTML(keyword)
  399. escapedR, _ := regexp.Compile(escapedKey)
  400. ids = gulu.Str.RemoveDuplicatedElem(ids)
  401. var renameRoots []*ast.Node
  402. renameRootTitles := map[string]string{}
  403. cachedTrees := map[string]*parse.Tree{}
  404. historyDir, err := getHistoryDir(HistoryOpReplace, time.Now())
  405. if nil != err {
  406. logging.LogErrorf("get history dir failed: %s", err)
  407. return
  408. }
  409. if 1 > len(ids) {
  410. // `Replace All` is no longer affected by pagination https://github.com/siyuan-note/siyuan/issues/8265
  411. blocks, _, _, _ := FullTextSearchBlock(keyword, boxes, paths, types, method, orderBy, groupBy, 1, math.MaxInt)
  412. for _, block := range blocks {
  413. ids = append(ids, block.ID)
  414. }
  415. }
  416. for _, id := range ids {
  417. bt := treenode.GetBlockTree(id)
  418. if nil == bt {
  419. continue
  420. }
  421. tree := cachedTrees[bt.RootID]
  422. if nil != tree {
  423. continue
  424. }
  425. tree, _ = LoadTreeByBlockID(id)
  426. if nil == tree {
  427. continue
  428. }
  429. historyPath := filepath.Join(historyDir, tree.Box, tree.Path)
  430. if err = os.MkdirAll(filepath.Dir(historyPath), 0755); nil != err {
  431. logging.LogErrorf("generate history failed: %s", err)
  432. return
  433. }
  434. var data []byte
  435. if data, err = filelock.ReadFile(filepath.Join(util.DataDir, tree.Box, tree.Path)); err != nil {
  436. logging.LogErrorf("generate history failed: %s", err)
  437. return
  438. }
  439. if err = gulu.File.WriteFileSafer(historyPath, data, 0644); err != nil {
  440. logging.LogErrorf("generate history failed: %s", err)
  441. return
  442. }
  443. cachedTrees[bt.RootID] = tree
  444. }
  445. indexHistoryDir(filepath.Base(historyDir), util.NewLute())
  446. for i, id := range ids {
  447. bt := treenode.GetBlockTree(id)
  448. if nil == bt {
  449. continue
  450. }
  451. tree := cachedTrees[bt.RootID]
  452. if nil == tree {
  453. continue
  454. }
  455. node := treenode.GetNodeInTree(tree, id)
  456. if nil == node {
  457. continue
  458. }
  459. if ast.NodeDocument == node.Type {
  460. if !replaceTypes["docTitle"] {
  461. continue
  462. }
  463. title := node.IALAttr("title")
  464. if 0 == method {
  465. if strings.Contains(title, keyword) {
  466. docTitleReplacement := strings.ReplaceAll(replacement, "/", "")
  467. renameRootTitles[node.ID] = strings.ReplaceAll(title, keyword, docTitleReplacement)
  468. renameRoots = append(renameRoots, node)
  469. }
  470. } else if 3 == method {
  471. if nil != r && r.MatchString(title) {
  472. docTitleReplacement := strings.ReplaceAll(replacement, "/", "")
  473. renameRootTitles[node.ID] = r.ReplaceAllString(title, docTitleReplacement)
  474. renameRoots = append(renameRoots, node)
  475. }
  476. }
  477. } else {
  478. luteEngine := util.NewLute()
  479. var unlinks []*ast.Node
  480. ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
  481. if !entering {
  482. return ast.WalkContinue
  483. }
  484. switch n.Type {
  485. case ast.NodeText:
  486. if !replaceTypes["text"] {
  487. return ast.WalkContinue
  488. }
  489. if replaceTextNode(n, method, keyword, replacement, r, luteEngine) {
  490. unlinks = append(unlinks, n)
  491. }
  492. case ast.NodeLinkDest:
  493. if !replaceTypes["imgSrc"] {
  494. return ast.WalkContinue
  495. }
  496. replaceNodeTokens(n, method, keyword, replacement, r)
  497. case ast.NodeLinkText:
  498. if !replaceTypes["imgText"] {
  499. return ast.WalkContinue
  500. }
  501. replaceNodeTokens(n, method, keyword, replacement, r)
  502. case ast.NodeLinkTitle:
  503. if !replaceTypes["imgTitle"] {
  504. return ast.WalkContinue
  505. }
  506. replaceNodeTokens(n, method, keyword, replacement, r)
  507. case ast.NodeCodeBlockCode:
  508. if !replaceTypes["codeBlock"] {
  509. return ast.WalkContinue
  510. }
  511. replaceNodeTokens(n, method, keyword, replacement, r)
  512. case ast.NodeMathBlockContent:
  513. if !replaceTypes["mathBlock"] {
  514. return ast.WalkContinue
  515. }
  516. replaceNodeTokens(n, method, keyword, replacement, r)
  517. case ast.NodeHTMLBlock:
  518. if !replaceTypes["htmlBlock"] {
  519. return ast.WalkContinue
  520. }
  521. replaceNodeTokens(n, method, keyword, replacement, r)
  522. case ast.NodeTextMark:
  523. if n.IsTextMarkType("code") {
  524. if !replaceTypes["code"] {
  525. return ast.WalkContinue
  526. }
  527. if 0 == method {
  528. if strings.Contains(n.TextMarkTextContent, escapedKey) {
  529. n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, escapedKey, replacement)
  530. }
  531. } else if 3 == method {
  532. if nil != escapedR && escapedR.MatchString(n.TextMarkTextContent) {
  533. n.TextMarkTextContent = escapedR.ReplaceAllString(n.TextMarkTextContent, replacement)
  534. }
  535. }
  536. } else if n.IsTextMarkType("a") {
  537. if replaceTypes["aText"] {
  538. if 0 == method {
  539. if strings.Contains(n.TextMarkTextContent, keyword) {
  540. n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, keyword, replacement)
  541. }
  542. } else if 3 == method {
  543. if nil != r && r.MatchString(n.TextMarkTextContent) {
  544. n.TextMarkTextContent = r.ReplaceAllString(n.TextMarkTextContent, replacement)
  545. }
  546. }
  547. }
  548. if replaceTypes["aTitle"] {
  549. if 0 == method {
  550. if strings.Contains(n.TextMarkATitle, keyword) {
  551. n.TextMarkATitle = strings.ReplaceAll(n.TextMarkATitle, keyword, replacement)
  552. }
  553. } else if 3 == method {
  554. if nil != r && r.MatchString(n.TextMarkATitle) {
  555. n.TextMarkATitle = r.ReplaceAllString(n.TextMarkATitle, replacement)
  556. }
  557. }
  558. }
  559. if replaceTypes["aHref"] {
  560. if 0 == method {
  561. if strings.Contains(n.TextMarkAHref, keyword) {
  562. n.TextMarkAHref = strings.ReplaceAll(n.TextMarkAHref, keyword, replacement)
  563. }
  564. } else if 3 == method {
  565. if nil != r && r.MatchString(n.TextMarkAHref) {
  566. n.TextMarkAHref = r.ReplaceAllString(n.TextMarkAHref, replacement)
  567. }
  568. }
  569. }
  570. } else if n.IsTextMarkType("em") {
  571. if !replaceTypes["em"] {
  572. return ast.WalkContinue
  573. }
  574. replaceNodeTextMarkTextContent(n, method, keyword, replacement, r, "em")
  575. } else if n.IsTextMarkType("strong") {
  576. if !replaceTypes["strong"] {
  577. return ast.WalkContinue
  578. }
  579. replaceNodeTextMarkTextContent(n, method, keyword, replacement, r, "strong")
  580. } else if n.IsTextMarkType("kbd") {
  581. if !replaceTypes["kbd"] {
  582. return ast.WalkContinue
  583. }
  584. replaceNodeTextMarkTextContent(n, method, keyword, replacement, r, "kbd")
  585. } else if n.IsTextMarkType("mark") {
  586. if !replaceTypes["mark"] {
  587. return ast.WalkContinue
  588. }
  589. replaceNodeTextMarkTextContent(n, method, keyword, replacement, r, "mark")
  590. } else if n.IsTextMarkType("s") {
  591. if !replaceTypes["s"] {
  592. return ast.WalkContinue
  593. }
  594. replaceNodeTextMarkTextContent(n, method, keyword, replacement, r, "s")
  595. } else if n.IsTextMarkType("sub") {
  596. if !replaceTypes["sub"] {
  597. return ast.WalkContinue
  598. }
  599. replaceNodeTextMarkTextContent(n, method, keyword, replacement, r, "sub")
  600. } else if n.IsTextMarkType("sup") {
  601. if !replaceTypes["sup"] {
  602. return ast.WalkContinue
  603. }
  604. replaceNodeTextMarkTextContent(n, method, keyword, replacement, r, "sup")
  605. } else if n.IsTextMarkType("tag") {
  606. if !replaceTypes["tag"] {
  607. return ast.WalkContinue
  608. }
  609. replaceNodeTextMarkTextContent(n, method, keyword, replacement, r, "tag")
  610. } else if n.IsTextMarkType("u") {
  611. if !replaceTypes["u"] {
  612. return ast.WalkContinue
  613. }
  614. replaceNodeTextMarkTextContent(n, method, keyword, replacement, r, "u")
  615. } else if n.IsTextMarkType("inline-math") {
  616. if !replaceTypes["inlineMath"] {
  617. return ast.WalkContinue
  618. }
  619. if 0 == method {
  620. if strings.Contains(n.TextMarkInlineMathContent, keyword) {
  621. n.TextMarkInlineMathContent = strings.ReplaceAll(n.TextMarkInlineMathContent, keyword, replacement)
  622. }
  623. } else if 3 == method {
  624. if nil != r && r.MatchString(n.TextMarkInlineMathContent) {
  625. n.TextMarkInlineMathContent = r.ReplaceAllString(n.TextMarkInlineMathContent, replacement)
  626. }
  627. }
  628. } else if n.IsTextMarkType("inline-memo") {
  629. if !replaceTypes["inlineMemo"] {
  630. return ast.WalkContinue
  631. }
  632. if 0 == method {
  633. if strings.Contains(n.TextMarkInlineMemoContent, keyword) {
  634. n.TextMarkInlineMemoContent = strings.ReplaceAll(n.TextMarkInlineMemoContent, keyword, replacement)
  635. }
  636. } else if 3 == method {
  637. if nil != r && r.MatchString(n.TextMarkInlineMemoContent) {
  638. n.TextMarkInlineMemoContent = r.ReplaceAllString(n.TextMarkInlineMemoContent, replacement)
  639. }
  640. }
  641. } else if n.IsTextMarkType("text") {
  642. // Search and replace fails in some cases https://github.com/siyuan-note/siyuan/issues/10016
  643. if !replaceTypes["text"] {
  644. return ast.WalkContinue
  645. }
  646. replaceNodeTextMarkTextContent(n, method, keyword, replacement, r, "text")
  647. }
  648. }
  649. return ast.WalkContinue
  650. })
  651. for _, unlink := range unlinks {
  652. unlink.Unlink()
  653. }
  654. if err = writeTreeUpsertQueue(tree); nil != err {
  655. return
  656. }
  657. }
  658. util.PushEndlessProgress(fmt.Sprintf(Conf.Language(206), i+1, len(ids)))
  659. }
  660. for i, renameRoot := range renameRoots {
  661. newTitle := renameRootTitles[renameRoot.ID]
  662. RenameDoc(renameRoot.Box, renameRoot.Path, newTitle)
  663. util.PushEndlessProgress(fmt.Sprintf(Conf.Language(207), i+1, len(renameRoots)))
  664. }
  665. WaitForWritingFiles()
  666. if 0 < len(ids) {
  667. go func() {
  668. time.Sleep(time.Millisecond * 500)
  669. util.ReloadUI()
  670. }()
  671. }
  672. return
  673. }
  674. func replaceNodeTextMarkTextContent(n *ast.Node, method int, keyword string, replacement string, r *regexp.Regexp, typ string) {
  675. if 0 == method {
  676. if "tag" == typ {
  677. keyword = strings.TrimPrefix(keyword, "#")
  678. keyword = strings.TrimSuffix(keyword, "#")
  679. }
  680. if strings.Contains(n.TextMarkTextContent, keyword) {
  681. n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, keyword, replacement)
  682. }
  683. } else if 3 == method {
  684. if nil != r && r.MatchString(n.TextMarkTextContent) {
  685. n.TextMarkTextContent = r.ReplaceAllString(n.TextMarkTextContent, replacement)
  686. }
  687. }
  688. }
  689. // replaceTextNode 替换文本节点为其他节点。
  690. // Supports replacing text elements with other elements https://github.com/siyuan-note/siyuan/issues/11058
  691. func replaceTextNode(text *ast.Node, method int, keyword string, replacement string, r *regexp.Regexp, luteEngine *lute.Lute) bool {
  692. if 0 == method {
  693. if bytes.Contains(text.Tokens, []byte(keyword)) {
  694. newContent := bytes.ReplaceAll(text.Tokens, []byte(keyword), []byte(replacement))
  695. tree := parse.Inline("", newContent, luteEngine.ParseOptions)
  696. if nil == tree.Root.FirstChild {
  697. return false
  698. }
  699. parse.NestedInlines2FlattedSpans(tree, false)
  700. var replaceNodes []*ast.Node
  701. for rNode := tree.Root.FirstChild.FirstChild; nil != rNode; rNode = rNode.Next {
  702. replaceNodes = append(replaceNodes, rNode)
  703. }
  704. for _, rNode := range replaceNodes {
  705. text.InsertBefore(rNode)
  706. }
  707. return true
  708. }
  709. } else if 3 == method {
  710. if nil != r && r.MatchString(string(text.Tokens)) {
  711. newContent := []byte(r.ReplaceAllString(string(text.Tokens), replacement))
  712. tree := parse.Inline("", newContent, luteEngine.ParseOptions)
  713. if nil == tree.Root.FirstChild {
  714. return false
  715. }
  716. var replaceNodes []*ast.Node
  717. for rNode := tree.Root.FirstChild.FirstChild; nil != rNode; rNode = rNode.Next {
  718. replaceNodes = append(replaceNodes, rNode)
  719. }
  720. for _, rNode := range replaceNodes {
  721. text.InsertBefore(rNode)
  722. }
  723. return true
  724. }
  725. }
  726. return false
  727. }
  728. func replaceNodeTokens(n *ast.Node, method int, keyword string, replacement string, r *regexp.Regexp) {
  729. if 0 == method {
  730. if bytes.Contains(n.Tokens, []byte(keyword)) {
  731. n.Tokens = bytes.ReplaceAll(n.Tokens, []byte(keyword), []byte(replacement))
  732. }
  733. } else if 3 == method {
  734. if nil != r && r.MatchString(string(n.Tokens)) {
  735. n.Tokens = []byte(r.ReplaceAllString(string(n.Tokens), replacement))
  736. }
  737. }
  738. }
  739. // FullTextSearchBlock 搜索内容块。
  740. //
  741. // method:0:关键字,1:查询语法,2:SQL,3:正则表达式
  742. // orderBy: 0:按块类型(默认),1:按创建时间升序,2:按创建时间降序,3:按更新时间升序,4:按更新时间降序,5:按内容顺序(仅在按文档分组时),6:按相关度升序,7:按相关度降序
  743. // groupBy:0:不分组,1:按文档分组
  744. func FullTextSearchBlock(query string, boxes, paths []string, types map[string]bool, method, orderBy, groupBy, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount, pageCount int) {
  745. ret = []*Block{}
  746. if "" == query {
  747. return
  748. }
  749. trimQuery := strings.TrimSpace(query)
  750. if "" != trimQuery {
  751. query = trimQuery
  752. }
  753. beforeLen := 36
  754. var blocks []*Block
  755. orderByClause := buildOrderBy(query, method, orderBy)
  756. switch method {
  757. case 1: // 查询语法
  758. filter := buildTypeFilter(types)
  759. boxFilter := buildBoxesFilter(boxes)
  760. pathFilter := buildPathsFilter(paths)
  761. blocks, matchedBlockCount, matchedRootCount = fullTextSearchByQuerySyntax(query, boxFilter, pathFilter, filter, orderByClause, beforeLen, page, pageSize)
  762. case 2: // SQL
  763. blocks, matchedBlockCount, matchedRootCount = searchBySQL(query, beforeLen, page, pageSize)
  764. case 3: // 正则表达式
  765. typeFilter := buildTypeFilter(types)
  766. boxFilter := buildBoxesFilter(boxes)
  767. pathFilter := buildPathsFilter(paths)
  768. blocks, matchedBlockCount, matchedRootCount = fullTextSearchByRegexp(query, boxFilter, pathFilter, typeFilter, orderByClause, beforeLen, page, pageSize)
  769. default: // 关键字
  770. filter := buildTypeFilter(types)
  771. boxFilter := buildBoxesFilter(boxes)
  772. pathFilter := buildPathsFilter(paths)
  773. blocks, matchedBlockCount, matchedRootCount = fullTextSearchByKeyword(query, boxFilter, pathFilter, filter, orderByClause, beforeLen, page, pageSize)
  774. }
  775. pageCount = (matchedBlockCount + pageSize - 1) / pageSize
  776. switch groupBy {
  777. case 0: // 不分组
  778. ret = blocks
  779. case 1: // 按文档分组
  780. rootMap := map[string]bool{}
  781. var rootIDs []string
  782. contentSorts := map[string]int{}
  783. for _, b := range blocks {
  784. if _, ok := rootMap[b.RootID]; !ok {
  785. rootMap[b.RootID] = true
  786. rootIDs = append(rootIDs, b.RootID)
  787. tree, _ := LoadTreeByBlockID(b.RootID)
  788. if nil == tree {
  789. continue
  790. }
  791. if 5 == orderBy { // 按内容顺序(仅在按文档分组时)
  792. sort := 0
  793. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  794. if !entering || !n.IsBlock() {
  795. return ast.WalkContinue
  796. }
  797. contentSorts[n.ID] = sort
  798. sort++
  799. return ast.WalkContinue
  800. })
  801. }
  802. }
  803. }
  804. sqlRoots := sql.GetBlocks(rootIDs)
  805. roots := fromSQLBlocks(&sqlRoots, "", beforeLen)
  806. for _, root := range roots {
  807. for _, b := range blocks {
  808. if 5 == orderBy { // 按内容顺序(仅在按文档分组时)
  809. b.Sort = contentSorts[b.ID]
  810. }
  811. if b.RootID == root.ID {
  812. root.Children = append(root.Children, b)
  813. }
  814. }
  815. switch orderBy {
  816. case 1: //按创建时间升序
  817. sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Created < root.Children[j].Created })
  818. case 2: // 按创建时间降序
  819. sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Created > root.Children[j].Created })
  820. case 3: // 按更新时间升序
  821. sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Updated < root.Children[j].Updated })
  822. case 4: // 按更新时间降序
  823. sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Updated > root.Children[j].Updated })
  824. case 5: // 按内容顺序(仅在按文档分组时)
  825. sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Sort < root.Children[j].Sort })
  826. default: // 按块类型(默认)
  827. sort.Slice(root.Children, func(i, j int) bool { return root.Children[i].Sort < root.Children[j].Sort })
  828. }
  829. }
  830. switch orderBy {
  831. case 1: //按创建时间升序
  832. sort.Slice(roots, func(i, j int) bool { return roots[i].Created < roots[j].Created })
  833. case 2: // 按创建时间降序
  834. sort.Slice(roots, func(i, j int) bool { return roots[i].Created > roots[j].Created })
  835. case 3: // 按更新时间升序
  836. sort.Slice(roots, func(i, j int) bool { return roots[i].Updated < roots[j].Updated })
  837. case 4: // 按更新时间降序
  838. sort.Slice(roots, func(i, j int) bool { return roots[i].Updated > roots[j].Updated })
  839. case 5: // 按内容顺序(仅在按文档分组时)
  840. // 都是文档,不需要再次排序
  841. case 6, 7: // 按相关度
  842. // 已在 ORDER BY 中处理
  843. default: // 按块类型(默认)
  844. // 都是文档,不需要再次排序
  845. }
  846. ret = roots
  847. default:
  848. ret = blocks
  849. }
  850. if 1 > len(ret) {
  851. ret = []*Block{}
  852. }
  853. return
  854. }
  855. func buildBoxesFilter(boxes []string) string {
  856. if 0 == len(boxes) {
  857. return ""
  858. }
  859. builder := bytes.Buffer{}
  860. builder.WriteString(" AND (")
  861. for i, box := range boxes {
  862. builder.WriteString(fmt.Sprintf("box = '%s'", box))
  863. if i < len(boxes)-1 {
  864. builder.WriteString(" OR ")
  865. }
  866. }
  867. builder.WriteString(")")
  868. return builder.String()
  869. }
  870. func buildPathsFilter(paths []string) string {
  871. if 0 == len(paths) {
  872. return ""
  873. }
  874. builder := bytes.Buffer{}
  875. builder.WriteString(" AND (")
  876. for i, path := range paths {
  877. builder.WriteString(fmt.Sprintf("path LIKE '%s%%'", path))
  878. if i < len(paths)-1 {
  879. builder.WriteString(" OR ")
  880. }
  881. }
  882. builder.WriteString(")")
  883. return builder.String()
  884. }
  885. func buildOrderBy(query string, method, orderBy int) string {
  886. switch orderBy {
  887. case 1:
  888. return "ORDER BY created ASC"
  889. case 2:
  890. return "ORDER BY created DESC"
  891. case 3:
  892. return "ORDER BY updated ASC"
  893. case 4:
  894. return "ORDER BY updated DESC"
  895. case 6:
  896. if 0 != method && 1 != method {
  897. // 只有关键字搜索和查询语法搜索才支持按相关度升序 https://github.com/siyuan-note/siyuan/issues/7861
  898. return "ORDER BY sort DESC, updated DESC"
  899. }
  900. return "ORDER BY rank DESC" // 默认是按相关度降序,所以按相关度升序要反过来使用 DESC
  901. case 7:
  902. if 0 != method && 1 != method {
  903. return "ORDER BY sort ASC, updated DESC"
  904. }
  905. return "ORDER BY rank" // 默认是按相关度降序
  906. default:
  907. clause := "ORDER BY CASE " +
  908. "WHEN name = '${keyword}' THEN 10 " +
  909. "WHEN alias = '${keyword}' THEN 20 " +
  910. "WHEN name LIKE '%${keyword}%' THEN 50 " +
  911. "WHEN alias LIKE '%${keyword}%' THEN 60 " +
  912. "ELSE 65535 END ASC, sort ASC, updated DESC"
  913. clause = strings.ReplaceAll(clause, "${keyword}", strings.ReplaceAll(query, "'", "''"))
  914. return clause
  915. }
  916. }
  917. func buildTypeFilter(types map[string]bool) string {
  918. s := conf.NewSearch()
  919. if err := copier.Copy(s, Conf.Search); nil != err {
  920. logging.LogErrorf("copy search conf failed: %s", err)
  921. }
  922. if nil != types {
  923. s.Document = types["document"]
  924. s.Heading = types["heading"]
  925. s.List = types["list"]
  926. s.ListItem = types["listItem"]
  927. s.CodeBlock = types["codeBlock"]
  928. s.MathBlock = types["mathBlock"]
  929. s.Table = types["table"]
  930. s.Blockquote = types["blockquote"]
  931. s.SuperBlock = types["superBlock"]
  932. s.Paragraph = types["paragraph"]
  933. s.HTMLBlock = types["htmlBlock"]
  934. s.EmbedBlock = types["embedBlock"]
  935. s.DatabaseBlock = types["databaseBlock"]
  936. s.AudioBlock = types["audioBlock"]
  937. s.VideoBlock = types["videoBlock"]
  938. s.IFrameBlock = types["iframeBlock"]
  939. s.WidgetBlock = types["widgetBlock"]
  940. } else {
  941. s.Document = Conf.Search.Document
  942. s.Heading = Conf.Search.Heading
  943. s.List = Conf.Search.List
  944. s.ListItem = Conf.Search.ListItem
  945. s.CodeBlock = Conf.Search.CodeBlock
  946. s.MathBlock = Conf.Search.MathBlock
  947. s.Table = Conf.Search.Table
  948. s.Blockquote = Conf.Search.Blockquote
  949. s.SuperBlock = Conf.Search.SuperBlock
  950. s.Paragraph = Conf.Search.Paragraph
  951. s.HTMLBlock = Conf.Search.HTMLBlock
  952. s.EmbedBlock = Conf.Search.EmbedBlock
  953. s.DatabaseBlock = Conf.Search.DatabaseBlock
  954. s.AudioBlock = Conf.Search.AudioBlock
  955. s.VideoBlock = Conf.Search.VideoBlock
  956. s.IFrameBlock = Conf.Search.IFrameBlock
  957. s.WidgetBlock = Conf.Search.WidgetBlock
  958. }
  959. return s.TypeFilter()
  960. }
  961. func searchBySQL(stmt string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
  962. stmt = filterQueryInvisibleChars(stmt)
  963. stmt = strings.TrimSpace(stmt)
  964. blocks := sql.SelectBlocksRawStmt(stmt, page, pageSize)
  965. ret = fromSQLBlocks(&blocks, "", beforeLen)
  966. if 1 > len(ret) {
  967. ret = []*Block{}
  968. return
  969. }
  970. stmt = strings.ToLower(stmt)
  971. if strings.HasPrefix(stmt, "select a.* ") { // 多个搜索关键字匹配文档 https://github.com/siyuan-note/siyuan/issues/7350
  972. stmt = strings.ReplaceAll(stmt, "select a.* ", "select COUNT(a.id) AS `matches`, COUNT(DISTINCT(a.root_id)) AS `docs` ")
  973. } else {
  974. stmt = strings.ReplaceAll(stmt, "select * ", "select COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` ")
  975. }
  976. stmt = removeLimitClause(stmt)
  977. result, _ := sql.QueryNoLimit(stmt)
  978. if 1 > len(ret) {
  979. return
  980. }
  981. matchedBlockCount = int(result[0]["matches"].(int64))
  982. matchedRootCount = int(result[0]["docs"].(int64))
  983. return
  984. }
  985. func removeLimitClause(stmt string) string {
  986. parsedStmt, err := sqlparser.Parse(stmt)
  987. if nil != err {
  988. return stmt
  989. }
  990. switch parsedStmt.(type) {
  991. case *sqlparser.Select:
  992. slct := parsedStmt.(*sqlparser.Select)
  993. if nil != slct.Limit {
  994. slct.Limit = nil
  995. }
  996. stmt = sqlparser.String(slct)
  997. }
  998. return stmt
  999. }
  1000. func fullTextSearchRefBlock(keyword string, beforeLen int, onlyDoc bool) (ret []*Block) {
  1001. keyword = filterQueryInvisibleChars(keyword)
  1002. if id := extractID(keyword); "" != id {
  1003. ret, _, _ = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+id+"'", 36, 1, 32)
  1004. return
  1005. }
  1006. quotedKeyword := stringQuery(keyword)
  1007. table := "blocks_fts" // 大小写敏感
  1008. if !Conf.Search.CaseSensitive {
  1009. table = "blocks_fts_case_insensitive"
  1010. }
  1011. projections := "id, parent_id, root_id, hash, box, path, " +
  1012. "snippet(" + table + ", 6, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS hpath, " +
  1013. "snippet(" + table + ", 7, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS name, " +
  1014. "snippet(" + table + ", 8, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS alias, " +
  1015. "snippet(" + table + ", 9, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS memo, " +
  1016. "tag, " +
  1017. "snippet(" + table + ", 11, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 64) AS content, " +
  1018. "fcontent, markdown, length, type, subtype, ial, sort, created, updated"
  1019. stmt := "SELECT " + projections + " FROM " + table + " WHERE " + table + " MATCH '" + columnFilter() + ":(" + quotedKeyword + ")' AND type"
  1020. if onlyDoc {
  1021. stmt += " = 'd'"
  1022. } else {
  1023. stmt += " IN " + Conf.Search.TypeFilter()
  1024. }
  1025. if ignoreLines := getRefSearchIgnoreLines(); 0 < len(ignoreLines) {
  1026. // Support ignore search results https://github.com/siyuan-note/siyuan/issues/10089
  1027. notLike := bytes.Buffer{}
  1028. for _, line := range ignoreLines {
  1029. notLike.WriteString(" AND ")
  1030. notLike.WriteString(line)
  1031. }
  1032. stmt += notLike.String()
  1033. }
  1034. orderBy := ` ORDER BY CASE
  1035. WHEN name = '${keyword}' THEN 10
  1036. WHEN alias = '${keyword}' THEN 20
  1037. WHEN memo = '${keyword}' THEN 30
  1038. WHEN content = '${keyword}' and type = 'd' THEN 40
  1039. WHEN content LIKE '%${keyword}%' and type = 'd' THEN 41
  1040. WHEN name LIKE '%${keyword}%' THEN 50
  1041. WHEN alias LIKE '%${keyword}%' THEN 60
  1042. WHEN content = '${keyword}' and type = 'h' THEN 70
  1043. WHEN content LIKE '%${keyword}%' and type = 'h' THEN 71
  1044. WHEN fcontent = '${keyword}' and type = 'i' THEN 80
  1045. WHEN fcontent LIKE '%${keyword}%' and type = 'i' THEN 81
  1046. WHEN memo LIKE '%${keyword}%' THEN 90
  1047. WHEN content LIKE '%${keyword}%' and type != 'i' and type != 'l' THEN 100
  1048. ELSE 65535 END ASC, sort ASC, length ASC`
  1049. orderBy = strings.ReplaceAll(orderBy, "${keyword}", strings.ReplaceAll(keyword, "'", "''"))
  1050. stmt += orderBy + " LIMIT " + strconv.Itoa(Conf.Search.Limit)
  1051. blocks := sql.SelectBlocksRawStmtNoParse(stmt, Conf.Search.Limit)
  1052. ret = fromSQLBlocks(&blocks, "", beforeLen)
  1053. if 1 > len(ret) {
  1054. ret = []*Block{}
  1055. }
  1056. return
  1057. }
  1058. func extractID(content string) (ret string) {
  1059. // Improve block ref search ID extraction https://github.com/siyuan-note/siyuan/issues/10848
  1060. if 22 > len(content) {
  1061. return
  1062. }
  1063. // 从第一个字符开始循环,直到找到一个合法的 ID 为止
  1064. for i := 0; i < len(content)-21; i++ {
  1065. if ast.IsNodeIDPattern(content[i : i+22]) {
  1066. ret = content[i : i+22]
  1067. return
  1068. }
  1069. }
  1070. return
  1071. }
  1072. func fullTextSearchByQuerySyntax(query, boxFilter, pathFilter, typeFilter, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
  1073. query = filterQueryInvisibleChars(query)
  1074. if ast.IsNodeIDPattern(query) {
  1075. ret, matchedBlockCount, matchedRootCount = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+query+"'", beforeLen, page, pageSize)
  1076. return
  1077. }
  1078. return fullTextSearchByFTS(query, boxFilter, pathFilter, typeFilter, orderBy, beforeLen, page, pageSize)
  1079. }
  1080. func fullTextSearchByKeyword(query, boxFilter, pathFilter, typeFilter string, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
  1081. query = filterQueryInvisibleChars(query)
  1082. if ast.IsNodeIDPattern(query) {
  1083. ret, matchedBlockCount, matchedRootCount = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+query+"'", beforeLen, page, pageSize)
  1084. return
  1085. }
  1086. query = stringQuery(query)
  1087. return fullTextSearchByFTS(query, boxFilter, pathFilter, typeFilter, orderBy, beforeLen, page, pageSize)
  1088. }
  1089. func fullTextSearchByRegexp(exp, boxFilter, pathFilter, typeFilter, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
  1090. exp = filterQueryInvisibleChars(exp)
  1091. fieldFilter := fieldRegexp(exp)
  1092. stmt := "SELECT * FROM `blocks` WHERE " + fieldFilter + " AND type IN " + typeFilter
  1093. stmt += boxFilter + pathFilter
  1094. stmt += " " + orderBy
  1095. stmt += " LIMIT " + strconv.Itoa(pageSize) + " OFFSET " + strconv.Itoa((page-1)*pageSize)
  1096. blocks := sql.SelectBlocksRawStmtNoParse(stmt, Conf.Search.Limit)
  1097. ret = fromSQLBlocks(&blocks, "", beforeLen)
  1098. if 1 > len(ret) {
  1099. ret = []*Block{}
  1100. }
  1101. matchedBlockCount, matchedRootCount = fullTextSearchCountByRegexp(exp, boxFilter, pathFilter, typeFilter)
  1102. return
  1103. }
  1104. func fullTextSearchCountByRegexp(exp, boxFilter, pathFilter, typeFilter string) (matchedBlockCount, matchedRootCount int) {
  1105. fieldFilter := fieldRegexp(exp)
  1106. stmt := "SELECT COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` FROM `blocks` WHERE " + fieldFilter + " AND type IN " + typeFilter
  1107. stmt += boxFilter + pathFilter
  1108. result, _ := sql.QueryNoLimit(stmt)
  1109. if 1 > len(result) {
  1110. return
  1111. }
  1112. matchedBlockCount = int(result[0]["matches"].(int64))
  1113. matchedRootCount = int(result[0]["docs"].(int64))
  1114. return
  1115. }
  1116. func fullTextSearchByFTS(query, boxFilter, pathFilter, typeFilter, orderBy string, beforeLen, page, pageSize int) (ret []*Block, matchedBlockCount, matchedRootCount int) {
  1117. table := "blocks_fts" // 大小写敏感
  1118. if !Conf.Search.CaseSensitive {
  1119. table = "blocks_fts_case_insensitive"
  1120. }
  1121. projections := "id, parent_id, root_id, hash, box, path, " +
  1122. // Search result content snippet returns more text https://github.com/siyuan-note/siyuan/issues/10707
  1123. "snippet(" + table + ", 6, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 512) AS hpath, " +
  1124. "snippet(" + table + ", 7, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 512) AS name, " +
  1125. "snippet(" + table + ", 8, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 512) AS alias, " +
  1126. "snippet(" + table + ", 9, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 512) AS memo, " +
  1127. "tag, " +
  1128. "snippet(" + table + ", 11, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "', '...', 512) AS content, " +
  1129. "fcontent, markdown, length, type, subtype, ial, sort, created, updated"
  1130. stmt := "SELECT " + projections + " FROM " + table + " WHERE (`" + table + "` MATCH '" + columnFilter() + ":(" + query + ")'"
  1131. stmt += ") AND type IN " + typeFilter
  1132. stmt += boxFilter + pathFilter
  1133. if ignoreLines := getSearchIgnoreLines(); 0 < len(ignoreLines) {
  1134. // Support ignore search results https://github.com/siyuan-note/siyuan/issues/10089
  1135. notLike := bytes.Buffer{}
  1136. for _, line := range ignoreLines {
  1137. notLike.WriteString(" AND ")
  1138. notLike.WriteString(line)
  1139. }
  1140. stmt += notLike.String()
  1141. }
  1142. stmt += " " + orderBy
  1143. stmt += " LIMIT " + strconv.Itoa(pageSize) + " OFFSET " + strconv.Itoa((page-1)*pageSize)
  1144. blocks := sql.SelectBlocksRawStmt(stmt, page, pageSize)
  1145. ret = fromSQLBlocks(&blocks, "", beforeLen)
  1146. if 1 > len(ret) {
  1147. ret = []*Block{}
  1148. }
  1149. matchedBlockCount, matchedRootCount = fullTextSearchCount(query, boxFilter, pathFilter, typeFilter)
  1150. return
  1151. }
  1152. func highlightByQuery(query, typeFilter, id string) (ret []string) {
  1153. const limit = 256
  1154. table := "blocks_fts"
  1155. if !Conf.Search.CaseSensitive {
  1156. table = "blocks_fts_case_insensitive"
  1157. }
  1158. projections := "id, parent_id, root_id, hash, box, path, " +
  1159. "highlight(" + table + ", 6, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS hpath, " +
  1160. "highlight(" + table + ", 7, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS name, " +
  1161. "highlight(" + table + ", 8, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS alias, " +
  1162. "highlight(" + table + ", 9, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS memo, " +
  1163. "tag, " +
  1164. "highlight(" + table + ", 11, '" + search.SearchMarkLeft + "', '" + search.SearchMarkRight + "') AS content, " +
  1165. "fcontent, markdown, length, type, subtype, ial, sort, created, updated"
  1166. stmt := "SELECT " + projections + " FROM " + table + " WHERE (`" + table + "` MATCH '" + columnFilter() + ":(" + query + ")'"
  1167. stmt += ") AND type IN " + typeFilter
  1168. stmt += " AND root_id = '" + id + "'"
  1169. stmt += " LIMIT " + strconv.Itoa(limit)
  1170. sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, limit)
  1171. for _, block := range sqlBlocks {
  1172. keyword := gulu.Str.SubstringsBetween(block.Content, search.SearchMarkLeft, search.SearchMarkRight)
  1173. if 0 < len(keyword) {
  1174. ret = append(ret, keyword...)
  1175. }
  1176. }
  1177. ret = gulu.Str.RemoveDuplicatedElem(ret)
  1178. return
  1179. }
  1180. func fullTextSearchCount(query, boxFilter, pathFilter, typeFilter string) (matchedBlockCount, matchedRootCount int) {
  1181. query = filterQueryInvisibleChars(query)
  1182. if ast.IsNodeIDPattern(query) {
  1183. ret, _ := sql.QueryNoLimit("SELECT COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` FROM `blocks` WHERE `id` = '" + query + "'")
  1184. if 1 > len(ret) {
  1185. return
  1186. }
  1187. matchedBlockCount = int(ret[0]["matches"].(int64))
  1188. matchedRootCount = int(ret[0]["docs"].(int64))
  1189. return
  1190. }
  1191. table := "blocks_fts" // 大小写敏感
  1192. if !Conf.Search.CaseSensitive {
  1193. table = "blocks_fts_case_insensitive"
  1194. }
  1195. stmt := "SELECT COUNT(id) AS `matches`, COUNT(DISTINCT(root_id)) AS `docs` FROM `" + table + "` WHERE (`" + table + "` MATCH '" + columnFilter() + ":(" + query + ")'"
  1196. stmt += ") AND type IN " + typeFilter
  1197. stmt += boxFilter + pathFilter
  1198. result, _ := sql.QueryNoLimit(stmt)
  1199. if 1 > len(result) {
  1200. return
  1201. }
  1202. matchedBlockCount = int(result[0]["matches"].(int64))
  1203. matchedRootCount = int(result[0]["docs"].(int64))
  1204. return
  1205. }
  1206. func markSearch(text string, keyword string, beforeLen int) (marked string, score float64) {
  1207. if 0 == len(keyword) {
  1208. marked = text
  1209. if strings.Contains(marked, search.SearchMarkLeft) { // 使用 FTS snippet() 处理过高亮片段,这里简单替换后就返回
  1210. marked = util.EscapeHTML(text)
  1211. marked = strings.ReplaceAll(marked, search.SearchMarkLeft, "<mark>")
  1212. marked = strings.ReplaceAll(marked, search.SearchMarkRight, "</mark>")
  1213. return
  1214. }
  1215. keywords := gulu.Str.SubstringsBetween(marked, search.SearchMarkLeft, search.SearchMarkRight)
  1216. keywords = gulu.Str.RemoveDuplicatedElem(keywords)
  1217. keyword = strings.Join(keywords, search.TermSep)
  1218. marked = strings.ReplaceAll(marked, search.SearchMarkLeft, "")
  1219. marked = strings.ReplaceAll(marked, search.SearchMarkRight, "")
  1220. _, marked = search.MarkText(marked, keyword, beforeLen, Conf.Search.CaseSensitive)
  1221. return
  1222. }
  1223. pos, marked := search.MarkText(text, keyword, beforeLen, Conf.Search.CaseSensitive)
  1224. if -1 < pos {
  1225. if 0 == pos {
  1226. score = 1
  1227. }
  1228. score += float64(strings.Count(marked, "<mark>"))
  1229. winkler := smetrics.JaroWinkler(text, keyword, 0.7, 4)
  1230. score += winkler
  1231. }
  1232. score = -score // 分越小排序越靠前
  1233. return
  1234. }
  1235. func fromSQLBlocks(sqlBlocks *[]*sql.Block, terms string, beforeLen int) (ret []*Block) {
  1236. for _, sqlBlock := range *sqlBlocks {
  1237. ret = append(ret, fromSQLBlock(sqlBlock, terms, beforeLen))
  1238. }
  1239. return
  1240. }
  1241. func fromSQLBlock(sqlBlock *sql.Block, terms string, beforeLen int) (block *Block) {
  1242. if nil == sqlBlock {
  1243. return
  1244. }
  1245. id := sqlBlock.ID
  1246. content := sqlBlock.Content
  1247. if 1 < strings.Count(content, search.SearchMarkRight) && strings.HasSuffix(content, search.SearchMarkRight+"...") {
  1248. // 返回多个关键字命中时需要检查最后一个关键字是否被截断
  1249. firstKeyword := gulu.Str.SubStringBetween(content, search.SearchMarkLeft, search.SearchMarkRight)
  1250. lastKeyword := gulu.Str.LastSubStringBetween(content, search.SearchMarkLeft, search.SearchMarkRight)
  1251. if firstKeyword != lastKeyword {
  1252. // 如果第一个关键字和最后一个关键字不相同,说明最后一个关键字被截断了
  1253. // 此时需要将 content 中的最后一个关键字替换为完整的关键字
  1254. content = strings.TrimSuffix(content, search.SearchMarkLeft+lastKeyword+search.SearchMarkRight+"...")
  1255. content += search.SearchMarkLeft + firstKeyword + search.SearchMarkRight + "..."
  1256. }
  1257. }
  1258. content = util.EscapeHTML(content) // Search dialog XSS https://github.com/siyuan-note/siyuan/issues/8525
  1259. content, _ = markSearch(content, terms, beforeLen)
  1260. content = maxContent(content, 5120)
  1261. markdown := maxContent(sqlBlock.Markdown, 5120)
  1262. fContent := util.EscapeHTML(sqlBlock.FContent) // fContent 会用于和 content 对比,在反链计算时用于判断是否是列表项下第一个子块,所以也需要转义 https://github.com/siyuan-note/siyuan/issues/11001
  1263. block = &Block{
  1264. Box: sqlBlock.Box,
  1265. Path: sqlBlock.Path,
  1266. ID: id,
  1267. RootID: sqlBlock.RootID,
  1268. ParentID: sqlBlock.ParentID,
  1269. Alias: sqlBlock.Alias,
  1270. Name: sqlBlock.Name,
  1271. Memo: sqlBlock.Memo,
  1272. Tag: sqlBlock.Tag,
  1273. Content: content,
  1274. FContent: fContent,
  1275. Markdown: markdown,
  1276. Type: treenode.FromAbbrType(sqlBlock.Type),
  1277. SubType: sqlBlock.SubType,
  1278. Sort: sqlBlock.Sort,
  1279. }
  1280. if "" != sqlBlock.IAL {
  1281. block.IAL = map[string]string{}
  1282. ialStr := strings.TrimPrefix(sqlBlock.IAL, "{:")
  1283. ialStr = strings.TrimSuffix(ialStr, "}")
  1284. ial := parse.Tokens2IAL([]byte(ialStr))
  1285. for _, kv := range ial {
  1286. block.IAL[kv[0]] = kv[1]
  1287. }
  1288. }
  1289. hPath, _ := markSearch(sqlBlock.HPath, terms, 18)
  1290. if !strings.HasPrefix(hPath, "/") {
  1291. hPath = "/" + hPath
  1292. }
  1293. block.HPath = hPath
  1294. if "" != block.Name {
  1295. block.Name, _ = markSearch(block.Name, terms, 256)
  1296. }
  1297. if "" != block.Alias {
  1298. block.Alias, _ = markSearch(block.Alias, terms, 256)
  1299. }
  1300. if "" != block.Memo {
  1301. block.Memo, _ = markSearch(block.Memo, terms, 256)
  1302. }
  1303. return
  1304. }
  1305. func maxContent(content string, maxLen int) string {
  1306. idx := strings.Index(content, "<mark>")
  1307. if 128 < maxLen && maxLen <= idx {
  1308. head := bytes.Buffer{}
  1309. for i := 0; i < 512; i++ {
  1310. r, size := utf8.DecodeLastRuneInString(content[:idx])
  1311. head.WriteRune(r)
  1312. idx -= size
  1313. if 64 < head.Len() {
  1314. break
  1315. }
  1316. }
  1317. content = util.Reverse(head.String()) + content[idx:]
  1318. }
  1319. if maxLen < utf8.RuneCountInString(content) {
  1320. return gulu.Str.SubStr(content, maxLen) + "..."
  1321. }
  1322. return content
  1323. }
  1324. func fieldRegexp(regexp string) string {
  1325. buf := bytes.Buffer{}
  1326. buf.WriteString("(")
  1327. buf.WriteString("content REGEXP '")
  1328. buf.WriteString(regexp)
  1329. buf.WriteString("'")
  1330. if Conf.Search.Name {
  1331. buf.WriteString(" OR name REGEXP '")
  1332. buf.WriteString(regexp)
  1333. buf.WriteString("'")
  1334. }
  1335. if Conf.Search.Alias {
  1336. buf.WriteString(" OR alias REGEXP '")
  1337. buf.WriteString(regexp)
  1338. buf.WriteString("'")
  1339. }
  1340. if Conf.Search.Memo {
  1341. buf.WriteString(" OR memo REGEXP '")
  1342. buf.WriteString(regexp)
  1343. buf.WriteString("'")
  1344. }
  1345. if Conf.Search.IAL {
  1346. buf.WriteString(" OR ial REGEXP '")
  1347. buf.WriteString(regexp)
  1348. buf.WriteString("'")
  1349. }
  1350. buf.WriteString(" OR tag REGEXP '")
  1351. buf.WriteString(regexp)
  1352. buf.WriteString("')")
  1353. return buf.String()
  1354. }
  1355. func columnFilter() string {
  1356. buf := bytes.Buffer{}
  1357. buf.WriteString("{content")
  1358. if Conf.Search.Name {
  1359. buf.WriteString(" name")
  1360. }
  1361. if Conf.Search.Alias {
  1362. buf.WriteString(" alias")
  1363. }
  1364. if Conf.Search.Memo {
  1365. buf.WriteString(" memo")
  1366. }
  1367. if Conf.Search.IAL {
  1368. buf.WriteString(" ial")
  1369. }
  1370. buf.WriteString(" tag}")
  1371. return buf.String()
  1372. }
  1373. func stringQuery(query string) string {
  1374. if "" == strings.TrimSpace(query) {
  1375. return "\"" + query + "\""
  1376. }
  1377. query = strings.ReplaceAll(query, "\"", "\"\"")
  1378. query = strings.ReplaceAll(query, "'", "''")
  1379. buf := bytes.Buffer{}
  1380. parts := strings.Split(query, " ")
  1381. for _, part := range parts {
  1382. part = strings.TrimSpace(part)
  1383. part = "\"" + part + "\""
  1384. buf.WriteString(part)
  1385. buf.WriteString(" ")
  1386. }
  1387. return strings.TrimSpace(buf.String())
  1388. }
  1389. // markReplaceSpan 用于处理搜索高亮。
  1390. func markReplaceSpan(n *ast.Node, unlinks *[]*ast.Node, keywords []string, markSpanDataType string, luteEngine *lute.Lute) bool {
  1391. if ast.NodeText == n.Type {
  1392. text := n.Content()
  1393. escapedText := util.EscapeHTML(text)
  1394. escapedKeywords := make([]string, len(keywords))
  1395. for i, keyword := range keywords {
  1396. escapedKeywords[i] = util.EscapeHTML(keyword)
  1397. }
  1398. hText := search.EncloseHighlighting(escapedText, escapedKeywords, search.GetMarkSpanStart(markSpanDataType), search.GetMarkSpanEnd(), Conf.Search.CaseSensitive, false)
  1399. if hText != escapedText {
  1400. text = hText
  1401. }
  1402. n.Tokens = gulu.Str.ToBytes(text)
  1403. if bytes.Contains(n.Tokens, []byte(search.MarkDataType)) {
  1404. linkTree := parse.Inline("", n.Tokens, luteEngine.ParseOptions)
  1405. var children []*ast.Node
  1406. for c := linkTree.Root.FirstChild.FirstChild; nil != c; c = c.Next {
  1407. children = append(children, c)
  1408. }
  1409. for _, c := range children {
  1410. n.InsertBefore(c)
  1411. }
  1412. *unlinks = append(*unlinks, n)
  1413. return true
  1414. }
  1415. } else if ast.NodeTextMark == n.Type {
  1416. // 搜索结果高亮支持大部分行级元素 https://github.com/siyuan-note/siyuan/issues/6745
  1417. if n.IsTextMarkType("inline-math") || n.IsTextMarkType("inline-memo") {
  1418. return false
  1419. }
  1420. var text string
  1421. if n.IsTextMarkType("code") {
  1422. // code 在前面的 n.
  1423. for i, k := range keywords {
  1424. keywords[i] = html.EscapeString(k)
  1425. }
  1426. text = n.TextMarkTextContent
  1427. } else {
  1428. text = n.Content()
  1429. }
  1430. startTag := search.GetMarkSpanStart(markSpanDataType)
  1431. text = search.EncloseHighlighting(text, keywords, startTag, search.GetMarkSpanEnd(), Conf.Search.CaseSensitive, false)
  1432. if strings.Contains(text, search.MarkDataType) {
  1433. dataType := search.GetMarkSpanStart(n.TextMarkType + " " + search.MarkDataType)
  1434. text = strings.ReplaceAll(text, startTag, dataType)
  1435. tokens := gulu.Str.ToBytes(text)
  1436. linkTree := parse.Inline("", tokens, luteEngine.ParseOptions)
  1437. var children []*ast.Node
  1438. for c := linkTree.Root.FirstChild.FirstChild; nil != c; c = c.Next {
  1439. if ast.NodeText == c.Type {
  1440. c.Type = ast.NodeTextMark
  1441. c.TextMarkType = n.TextMarkType
  1442. c.TextMarkTextContent = string(c.Tokens)
  1443. if n.IsTextMarkType("a") {
  1444. c.TextMarkAHref, c.TextMarkATitle = n.TextMarkAHref, n.TextMarkATitle
  1445. } else if treenode.IsBlockRef(n) {
  1446. c.TextMarkBlockRefID = n.TextMarkBlockRefID
  1447. c.TextMarkBlockRefSubtype = n.TextMarkBlockRefSubtype
  1448. } else if treenode.IsFileAnnotationRef(n) {
  1449. c.TextMarkFileAnnotationRefID = n.TextMarkFileAnnotationRefID
  1450. }
  1451. } else if ast.NodeTextMark == c.Type {
  1452. if n.IsTextMarkType("a") {
  1453. c.TextMarkAHref, c.TextMarkATitle = n.TextMarkAHref, n.TextMarkATitle
  1454. } else if treenode.IsBlockRef(n) {
  1455. c.TextMarkBlockRefID = n.TextMarkBlockRefID
  1456. c.TextMarkBlockRefSubtype = n.TextMarkBlockRefSubtype
  1457. } else if treenode.IsFileAnnotationRef(n) {
  1458. c.TextMarkFileAnnotationRefID = n.TextMarkFileAnnotationRefID
  1459. }
  1460. }
  1461. children = append(children, c)
  1462. if nil != n.Next && ast.NodeKramdownSpanIAL == n.Next.Type {
  1463. c.KramdownIAL = n.KramdownIAL
  1464. ial := &ast.Node{Type: ast.NodeKramdownSpanIAL, Tokens: n.Next.Tokens}
  1465. children = append(children, ial)
  1466. }
  1467. }
  1468. for _, c := range children {
  1469. n.InsertBefore(c)
  1470. }
  1471. *unlinks = append(*unlinks, n)
  1472. return true
  1473. }
  1474. }
  1475. return false
  1476. }
  1477. // markReplaceSpanWithSplit 用于处理虚拟引用和反链提及高亮。
  1478. func markReplaceSpanWithSplit(text string, keywords []string, replacementStart, replacementEnd string) (ret string) {
  1479. // 虚拟引用和反链提及关键字按最长匹配优先 https://github.com/siyuan-note/siyuan/issues/7465
  1480. sort.Slice(keywords, func(i, j int) bool { return len(keywords[i]) > len(keywords[j]) })
  1481. tmp := search.EncloseHighlighting(text, keywords, replacementStart, replacementEnd, Conf.Search.CaseSensitive, true)
  1482. parts := strings.Split(tmp, replacementEnd)
  1483. buf := bytes.Buffer{}
  1484. for i := 0; i < len(parts); i++ {
  1485. if i >= len(parts)-1 {
  1486. buf.WriteString(parts[i])
  1487. break
  1488. }
  1489. if nextPart := parts[i+1]; 0 < len(nextPart) && lex.IsASCIILetter(nextPart[0]) {
  1490. // 取消已经高亮的部分
  1491. part := strings.ReplaceAll(parts[i], replacementStart, "")
  1492. buf.WriteString(part)
  1493. continue
  1494. }
  1495. buf.WriteString(parts[i])
  1496. buf.WriteString(replacementEnd)
  1497. }
  1498. ret = buf.String()
  1499. return
  1500. }
  1501. var (
  1502. searchIgnoreLastModified int64
  1503. searchIgnore []string
  1504. searchIgnoreLock = sync.Mutex{}
  1505. )
  1506. func getSearchIgnoreLines() (ret []string) {
  1507. // Support ignore search results https://github.com/siyuan-note/siyuan/issues/10089
  1508. now := time.Now().UnixMilli()
  1509. if now-searchIgnoreLastModified < 30*1000 {
  1510. return searchIgnore
  1511. }
  1512. searchIgnoreLock.Lock()
  1513. defer searchIgnoreLock.Unlock()
  1514. searchIgnoreLastModified = now
  1515. searchIgnorePath := filepath.Join(util.DataDir, ".siyuan", "searchignore")
  1516. err := os.MkdirAll(filepath.Dir(searchIgnorePath), 0755)
  1517. if nil != err {
  1518. return
  1519. }
  1520. if !gulu.File.IsExist(searchIgnorePath) {
  1521. if err = gulu.File.WriteFileSafer(searchIgnorePath, nil, 0644); nil != err {
  1522. logging.LogErrorf("create searchignore [%s] failed: %s", searchIgnorePath, err)
  1523. return
  1524. }
  1525. }
  1526. data, err := os.ReadFile(searchIgnorePath)
  1527. if nil != err {
  1528. logging.LogErrorf("read searchignore [%s] failed: %s", searchIgnorePath, err)
  1529. return
  1530. }
  1531. dataStr := string(data)
  1532. dataStr = strings.ReplaceAll(dataStr, "\r\n", "\n")
  1533. ret = strings.Split(dataStr, "\n")
  1534. ret = gulu.Str.RemoveDuplicatedElem(ret)
  1535. if 0 < len(ret) && "" == ret[0] {
  1536. ret = ret[1:]
  1537. }
  1538. searchIgnore = nil
  1539. for _, line := range ret {
  1540. searchIgnore = append(searchIgnore, line)
  1541. }
  1542. return
  1543. }
  1544. var (
  1545. refSearchIgnoreLastModified int64
  1546. refSearchIgnore []string
  1547. refSearchIgnoreLock = sync.Mutex{}
  1548. )
  1549. func getRefSearchIgnoreLines() (ret []string) {
  1550. // Support ignore search results https://github.com/siyuan-note/siyuan/issues/10089
  1551. now := time.Now().UnixMilli()
  1552. if now-refSearchIgnoreLastModified < 30*1000 {
  1553. return refSearchIgnore
  1554. }
  1555. refSearchIgnoreLock.Lock()
  1556. defer refSearchIgnoreLock.Unlock()
  1557. refSearchIgnoreLastModified = now
  1558. searchIgnorePath := filepath.Join(util.DataDir, ".siyuan", "refsearchignore")
  1559. err := os.MkdirAll(filepath.Dir(searchIgnorePath), 0755)
  1560. if nil != err {
  1561. return
  1562. }
  1563. if !gulu.File.IsExist(searchIgnorePath) {
  1564. if err = gulu.File.WriteFileSafer(searchIgnorePath, nil, 0644); nil != err {
  1565. logging.LogErrorf("create refsearchignore [%s] failed: %s", searchIgnorePath, err)
  1566. return
  1567. }
  1568. }
  1569. data, err := os.ReadFile(searchIgnorePath)
  1570. if nil != err {
  1571. logging.LogErrorf("read refsearchignore [%s] failed: %s", searchIgnorePath, err)
  1572. return
  1573. }
  1574. dataStr := string(data)
  1575. dataStr = strings.ReplaceAll(dataStr, "\r\n", "\n")
  1576. ret = strings.Split(dataStr, "\n")
  1577. ret = gulu.Str.RemoveDuplicatedElem(ret)
  1578. if 0 < len(ret) && "" == ret[0] {
  1579. ret = ret[1:]
  1580. }
  1581. refSearchIgnore = nil
  1582. for _, line := range ret {
  1583. refSearchIgnore = append(refSearchIgnore, line)
  1584. }
  1585. return
  1586. }
  1587. func filterQueryInvisibleChars(query string) string {
  1588. query = strings.ReplaceAll(query, " ", "_@full_width_space@_")
  1589. query = gulu.Str.RemoveInvisible(query)
  1590. query = strings.ReplaceAll(query, "_@full_width_space@_", " ")
  1591. return query
  1592. }