graph.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  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. "github.com/siyuan-note/siyuan/kernel/util"
  20. "math"
  21. "strings"
  22. "unicode/utf8"
  23. "github.com/88250/gulu"
  24. "github.com/88250/lute/ast"
  25. "github.com/88250/lute/html"
  26. "github.com/88250/lute/parse"
  27. "github.com/siyuan-note/logging"
  28. "github.com/siyuan-note/siyuan/kernel/sql"
  29. "github.com/siyuan-note/siyuan/kernel/treenode"
  30. )
  31. type GraphNode struct {
  32. ID string `json:"id"`
  33. Box string `json:"box"`
  34. Path string `json:"path"`
  35. Size float64 `json:"size"`
  36. Title string `json:"title,omitempty"`
  37. Label string `json:"label"`
  38. Type string `json:"type"`
  39. Refs int `json:"refs"`
  40. Defs int `json:"defs"`
  41. }
  42. type GraphLink struct {
  43. From string `json:"from"`
  44. To string `json:"to"`
  45. Ref bool `json:"ref"`
  46. Arrows *GraphArrows `json:"arrows"`
  47. }
  48. type GraphArrows struct {
  49. To *GraphArrowsTo `json:"to"`
  50. }
  51. type GraphArrowsTo struct {
  52. Enabled bool `json:"enabled"`
  53. }
  54. func BuildTreeGraph(id, query string) (boxID string, nodes []*GraphNode, links []*GraphLink) {
  55. nodes = []*GraphNode{}
  56. links = []*GraphLink{}
  57. tree, err := LoadTreeByBlockID(id)
  58. if nil != err {
  59. return
  60. }
  61. node := treenode.GetNodeInTree(tree, id)
  62. if nil == node {
  63. return
  64. }
  65. sqlBlock := sql.BuildBlockFromNode(node, tree)
  66. boxID = sqlBlock.Box
  67. block := fromSQLBlock(sqlBlock, "", 0)
  68. stmt := query2Stmt(query)
  69. stmt += graphTypeFilter(true)
  70. stmt += graphDailyNoteFilter(true)
  71. stmt = strings.ReplaceAll(stmt, "content", "ref.content")
  72. forwardlinks, backlinks := buildFullLinks(stmt)
  73. var sqlBlocks []*sql.Block
  74. var rootID string
  75. if ast.NodeDocument == node.Type {
  76. sqlBlocks = sql.GetAllChildBlocks([]string{block.ID}, stmt)
  77. rootID = block.ID
  78. } else {
  79. sqlBlocks = sql.GetChildBlocks(block.ID, stmt)
  80. }
  81. blocks := fromSQLBlocks(&sqlBlocks, "", 0)
  82. if "" != rootID {
  83. // 局部关系图中添加文档链接关系 https://github.com/siyuan-note/siyuan/issues/4996
  84. rootBlock := getBlockIn(blocks, rootID)
  85. if nil != rootBlock {
  86. // 按引用处理
  87. sqlRootDefs := sql.QueryDefRootBlocksByRefRootID(rootID)
  88. rootDefBlocks := fromSQLBlocks(&sqlRootDefs, "", 0)
  89. var rootIDs []string
  90. for _, rootDef := range rootDefBlocks {
  91. blocks = append(blocks, rootDef)
  92. rootIDs = append(rootIDs, rootDef.ID)
  93. }
  94. sqlRefBlocks := sql.QueryRefRootBlocksByDefRootIDs(rootIDs)
  95. for defRootID, sqlRefBs := range sqlRefBlocks {
  96. rootB := getBlockIn(rootDefBlocks, defRootID)
  97. if nil == rootB {
  98. continue
  99. }
  100. blocks = append(blocks, rootB)
  101. refBlocks := fromSQLBlocks(&sqlRefBs, "", 0)
  102. rootB.Refs = append(rootB.Refs, refBlocks...)
  103. blocks = append(blocks, refBlocks...)
  104. }
  105. // 按定义处理
  106. blocks = append(blocks, rootBlock)
  107. sqlRefBlocks = sql.QueryRefRootBlocksByDefRootIDs([]string{rootID})
  108. // 关系图日记过滤失效 https://github.com/siyuan-note/siyuan/issues/7547
  109. dailyNotesPaths := dailyNotePaths(true)
  110. for _, sqlRefBs := range sqlRefBlocks {
  111. refBlocks := fromSQLBlocks(&sqlRefBs, "", 0)
  112. if 0 < len(dailyNotesPaths) {
  113. filterDailyNote := false
  114. var tmp []*Block
  115. for _, refBlock := range refBlocks {
  116. for _, dailyNotePath := range dailyNotesPaths {
  117. if strings.HasPrefix(refBlock.HPath, dailyNotePath) {
  118. filterDailyNote = true
  119. break
  120. }
  121. }
  122. if !filterDailyNote {
  123. tmp = append(tmp, refBlock)
  124. }
  125. }
  126. refBlocks = tmp
  127. }
  128. rootBlock.Refs = append(rootBlock.Refs, refBlocks...)
  129. blocks = append(blocks, refBlocks...)
  130. }
  131. }
  132. }
  133. genTreeNodes(blocks, &nodes, &links, true)
  134. growTreeGraph(&forwardlinks, &backlinks, &nodes)
  135. blocks = append(blocks, forwardlinks...)
  136. blocks = append(blocks, backlinks...)
  137. buildLinks(&blocks, &links, true)
  138. if Conf.Graph.Local.Tag {
  139. p := sqlBlock.Path
  140. linkTagBlocks(&blocks, &nodes, &links, p)
  141. }
  142. markLinkedNodes(&nodes, &links, true)
  143. nodes = removeDuplicatedUnescape(nodes)
  144. return
  145. }
  146. func BuildGraph(query string) (boxID string, nodes []*GraphNode, links []*GraphLink) {
  147. nodes = []*GraphNode{}
  148. links = []*GraphLink{}
  149. stmt := query2Stmt(query)
  150. stmt = strings.TrimPrefix(stmt, "select * from blocks where")
  151. stmt += graphTypeFilter(false)
  152. stmt += graphDailyNoteFilter(false)
  153. stmt = strings.ReplaceAll(stmt, "content", "ref.content")
  154. forwardlinks, backlinks := buildFullLinks(stmt)
  155. var blocks []*Block
  156. roots := sql.GetAllRootBlocks()
  157. if 0 < len(roots) {
  158. boxID = roots[0].Box
  159. }
  160. var rootIDs []string
  161. for _, root := range roots {
  162. rootIDs = append(rootIDs, root.ID)
  163. }
  164. rootIDs = gulu.Str.RemoveDuplicatedElem(rootIDs)
  165. sqlBlocks := sql.GetAllChildBlocks(rootIDs, stmt)
  166. treeBlocks := fromSQLBlocks(&sqlBlocks, "", 0)
  167. genTreeNodes(treeBlocks, &nodes, &links, false)
  168. blocks = append(blocks, treeBlocks...)
  169. // 文档块关联
  170. sqlRootRefBlocks := sql.QueryRefRootBlocksByDefRootIDs(rootIDs)
  171. for defRootID, sqlRefBlocks := range sqlRootRefBlocks {
  172. rootBlock := getBlockIn(treeBlocks, defRootID)
  173. if nil == rootBlock {
  174. continue
  175. }
  176. refBlocks := fromSQLBlocks(&sqlRefBlocks, "", 0)
  177. rootBlock.Refs = append(rootBlock.Refs, refBlocks...)
  178. }
  179. growTreeGraph(&forwardlinks, &backlinks, &nodes)
  180. blocks = append(blocks, forwardlinks...)
  181. blocks = append(blocks, backlinks...)
  182. buildLinks(&blocks, &links, false)
  183. if Conf.Graph.Global.Tag {
  184. linkTagBlocks(&blocks, &nodes, &links, "")
  185. }
  186. markLinkedNodes(&nodes, &links, false)
  187. pruneUnref(&nodes, &links)
  188. nodes = removeDuplicatedUnescape(nodes)
  189. return
  190. }
  191. func linkTagBlocks(blocks *[]*Block, nodes *[]*GraphNode, links *[]*GraphLink, p string) {
  192. tagSpans := sql.QueryTagSpans(p)
  193. if 1 > len(tagSpans) {
  194. return
  195. }
  196. isGlobal := "" == p
  197. nodeSize := Conf.Graph.Global.NodeSize
  198. if !isGlobal {
  199. nodeSize = Conf.Graph.Local.NodeSize
  200. }
  201. // 构造标签节点
  202. var tagNodes []*GraphNode
  203. for _, tagSpan := range tagSpans {
  204. if nil == tagNodeIn(tagNodes, tagSpan.Content) {
  205. node := &GraphNode{
  206. ID: tagSpan.Content,
  207. Label: tagSpan.Content,
  208. Size: nodeSize,
  209. Type: tagSpan.Type,
  210. }
  211. *nodes = append(*nodes, node)
  212. tagNodes = append(tagNodes, node)
  213. }
  214. }
  215. // 连接标签和块
  216. for _, block := range *blocks {
  217. for _, tagSpan := range tagSpans {
  218. if isGlobal { // 全局关系图将标签链接到文档块上
  219. if block.RootID == tagSpan.RootID { // 局部关系图将标签链接到子块上
  220. *links = append(*links, &GraphLink{
  221. From: tagSpan.Content,
  222. To: block.RootID,
  223. })
  224. }
  225. } else {
  226. if block.ID == tagSpan.BlockID { // 局部关系图将标签链接到子块上
  227. *links = append(*links, &GraphLink{
  228. From: tagSpan.Content,
  229. To: block.ID,
  230. })
  231. }
  232. }
  233. }
  234. }
  235. // 连接层级标签
  236. for _, tagNode := range tagNodes {
  237. ids := strings.Split(tagNode.ID, "/")
  238. if 2 > len(ids) {
  239. continue
  240. }
  241. for _, targetID := range ids[:len(ids)-1] {
  242. if targetTag := tagNodeIn(tagNodes, targetID); nil != targetTag {
  243. *links = append(*links, &GraphLink{
  244. From: tagNode.ID,
  245. To: targetID,
  246. })
  247. }
  248. }
  249. }
  250. }
  251. func tagNodeIn(tagNodes []*GraphNode, content string) *GraphNode {
  252. for _, tagNode := range tagNodes {
  253. if tagNode.Label == content {
  254. return tagNode
  255. }
  256. }
  257. return nil
  258. }
  259. func growTreeGraph(forwardlinks, backlinks *[]*Block, nodes *[]*GraphNode) {
  260. forwardDepth, backDepth := 0, 0
  261. growLinkedNodes(forwardlinks, backlinks, nodes, nodes, &forwardDepth, &backDepth)
  262. }
  263. func growLinkedNodes(forwardlinks, backlinks *[]*Block, nodes, all *[]*GraphNode, forwardDepth, backDepth *int) {
  264. if 1 > len(*nodes) {
  265. return
  266. }
  267. forwardGeneration := &[]*GraphNode{}
  268. if 16 > *forwardDepth {
  269. for _, ref := range *forwardlinks {
  270. for _, node := range *nodes {
  271. if node.ID == ref.ID {
  272. var defs []*Block
  273. for _, refDef := range ref.Defs {
  274. if existNodes(all, refDef.ID) || existNodes(forwardGeneration, refDef.ID) || existNodes(nodes, refDef.ID) {
  275. continue
  276. }
  277. defs = append(defs, refDef)
  278. }
  279. for _, refDef := range defs {
  280. defNode := &GraphNode{
  281. ID: refDef.ID,
  282. Box: refDef.Box,
  283. Path: refDef.Path,
  284. Size: Conf.Graph.Local.NodeSize,
  285. Type: refDef.Type,
  286. }
  287. nodeTitleLabel(defNode, nodeContentByBlock(refDef))
  288. *forwardGeneration = append(*forwardGeneration, defNode)
  289. }
  290. }
  291. }
  292. }
  293. }
  294. backGeneration := &[]*GraphNode{}
  295. if 16 > *backDepth {
  296. for _, def := range *backlinks {
  297. for _, node := range *nodes {
  298. if node.ID == def.ID {
  299. for _, ref := range def.Refs {
  300. if existNodes(all, ref.ID) || existNodes(backGeneration, ref.ID) || existNodes(nodes, ref.ID) {
  301. continue
  302. }
  303. refNode := &GraphNode{
  304. ID: ref.ID,
  305. Box: ref.Box,
  306. Path: ref.Path,
  307. Size: Conf.Graph.Local.NodeSize,
  308. Type: ref.Type,
  309. }
  310. nodeTitleLabel(refNode, nodeContentByBlock(ref))
  311. *backGeneration = append(*backGeneration, refNode)
  312. }
  313. }
  314. }
  315. }
  316. }
  317. generation := &[]*GraphNode{}
  318. *generation = append(*generation, *forwardGeneration...)
  319. *generation = append(*generation, *backGeneration...)
  320. *forwardDepth++
  321. *backDepth++
  322. growLinkedNodes(forwardlinks, backlinks, generation, nodes, forwardDepth, backDepth)
  323. *nodes = append(*nodes, *generation...)
  324. }
  325. func existNodes(nodes *[]*GraphNode, id string) bool {
  326. for _, node := range *nodes {
  327. if node.ID == id {
  328. return true
  329. }
  330. }
  331. return false
  332. }
  333. func buildLinks(defs *[]*Block, links *[]*GraphLink, local bool) {
  334. for _, def := range *defs {
  335. for _, ref := range def.Refs {
  336. link := &GraphLink{
  337. From: ref.ID,
  338. To: def.ID,
  339. Ref: true,
  340. }
  341. if local {
  342. if Conf.Graph.Local.Arrow {
  343. link.Arrows = &GraphArrows{To: &GraphArrowsTo{Enabled: true}}
  344. }
  345. } else {
  346. if Conf.Graph.Global.Arrow {
  347. link.Arrows = &GraphArrows{To: &GraphArrowsTo{Enabled: true}}
  348. }
  349. }
  350. *links = append(*links, link)
  351. }
  352. }
  353. }
  354. func genTreeNodes(blocks []*Block, nodes *[]*GraphNode, links *[]*GraphLink, local bool) {
  355. nodeSize := Conf.Graph.Local.NodeSize
  356. if !local {
  357. nodeSize = Conf.Graph.Global.NodeSize
  358. }
  359. for _, block := range blocks {
  360. node := &GraphNode{
  361. ID: block.ID,
  362. Box: block.Box,
  363. Path: block.Path,
  364. Type: block.Type,
  365. Size: nodeSize,
  366. }
  367. nodeTitleLabel(node, nodeContentByBlock(block))
  368. *nodes = append(*nodes, node)
  369. *links = append(*links, &GraphLink{
  370. From: block.ParentID,
  371. To: block.ID,
  372. Ref: false,
  373. })
  374. }
  375. }
  376. func markLinkedNodes(nodes *[]*GraphNode, links *[]*GraphLink, local bool) {
  377. nodeSize := Conf.Graph.Local.NodeSize
  378. if !local {
  379. nodeSize = Conf.Graph.Global.NodeSize
  380. }
  381. tmpLinks := (*links)[:0]
  382. for _, link := range *links {
  383. var sourceFound, targetFound bool
  384. for _, node := range *nodes {
  385. if link.To == node.ID {
  386. if link.Ref {
  387. size := nodeSize
  388. node.Defs++
  389. size = math.Log2(float64(node.Defs))*nodeSize + nodeSize
  390. node.Size = size
  391. }
  392. targetFound = true
  393. } else if link.From == node.ID {
  394. node.Refs++
  395. sourceFound = true
  396. }
  397. if targetFound && sourceFound {
  398. break
  399. }
  400. }
  401. if sourceFound && targetFound {
  402. tmpLinks = append(tmpLinks, link)
  403. }
  404. }
  405. *links = tmpLinks
  406. }
  407. func removeDuplicatedUnescape(nodes []*GraphNode) (ret []*GraphNode) {
  408. m := map[string]*GraphNode{}
  409. for _, n := range nodes {
  410. if nil == m[n.ID] {
  411. n.Title = html.UnescapeString(n.Title)
  412. n.Label = html.UnescapeString(n.Label)
  413. ret = append(ret, n)
  414. m[n.ID] = n
  415. }
  416. }
  417. return ret
  418. }
  419. func pruneUnref(nodes *[]*GraphNode, links *[]*GraphLink) {
  420. maxBlocks := Conf.Graph.MaxBlocks
  421. tmpNodes := (*nodes)[:0]
  422. for _, node := range *nodes {
  423. if 0 == Conf.Graph.Global.MinRefs {
  424. tmpNodes = append(tmpNodes, node)
  425. } else {
  426. if Conf.Graph.Global.MinRefs <= node.Refs {
  427. tmpNodes = append(tmpNodes, node)
  428. continue
  429. }
  430. if Conf.Graph.Global.MinRefs <= node.Defs {
  431. tmpNodes = append(tmpNodes, node)
  432. continue
  433. }
  434. }
  435. if maxBlocks < len(tmpNodes) {
  436. logging.LogWarnf("exceeded the maximum number of render nodes [%d]", maxBlocks)
  437. break
  438. }
  439. }
  440. *nodes = tmpNodes
  441. tmpLinks := (*links)[:0]
  442. for _, link := range *links {
  443. var sourceFound, targetFound bool
  444. for _, node := range *nodes {
  445. if link.To == node.ID {
  446. targetFound = true
  447. } else if link.From == node.ID {
  448. sourceFound = true
  449. }
  450. }
  451. if sourceFound && targetFound {
  452. tmpLinks = append(tmpLinks, link)
  453. }
  454. }
  455. *links = tmpLinks
  456. }
  457. func nodeContentByBlock(block *Block) (ret string) {
  458. if ret = block.Name; "" != ret {
  459. return
  460. }
  461. if ret = block.Memo; "" != ret {
  462. return
  463. }
  464. ret = block.Content
  465. if maxLen := 48; maxLen < utf8.RuneCountInString(ret) {
  466. ret = gulu.Str.SubStr(ret, maxLen) + "..."
  467. }
  468. return
  469. }
  470. func graphTypeFilter(local bool) string {
  471. var inList []string
  472. paragraph := Conf.Graph.Local.Paragraph
  473. if !local {
  474. paragraph = Conf.Graph.Global.Paragraph
  475. }
  476. if paragraph {
  477. inList = append(inList, "'p'")
  478. }
  479. heading := Conf.Graph.Local.Heading
  480. if !local {
  481. heading = Conf.Graph.Global.Heading
  482. }
  483. if heading {
  484. inList = append(inList, "'h'")
  485. }
  486. math := Conf.Graph.Local.Math
  487. if !local {
  488. math = Conf.Graph.Global.Math
  489. }
  490. if math {
  491. inList = append(inList, "'m'")
  492. }
  493. code := Conf.Graph.Local.Code
  494. if !local {
  495. code = Conf.Graph.Global.Code
  496. }
  497. if code {
  498. inList = append(inList, "'c'")
  499. }
  500. table := Conf.Graph.Local.Table
  501. if !local {
  502. table = Conf.Graph.Global.Table
  503. }
  504. if table {
  505. inList = append(inList, "'t'")
  506. }
  507. list := Conf.Graph.Local.List
  508. if !local {
  509. list = Conf.Graph.Global.List
  510. }
  511. if list {
  512. inList = append(inList, "'l'")
  513. }
  514. listItem := Conf.Graph.Local.ListItem
  515. if !local {
  516. listItem = Conf.Graph.Global.ListItem
  517. }
  518. if listItem {
  519. inList = append(inList, "'i'")
  520. }
  521. blockquote := Conf.Graph.Local.Blockquote
  522. if !local {
  523. blockquote = Conf.Graph.Global.Blockquote
  524. }
  525. if blockquote {
  526. inList = append(inList, "'b'")
  527. }
  528. super := Conf.Graph.Local.Super
  529. if !local {
  530. super = Conf.Graph.Global.Super
  531. }
  532. if super {
  533. inList = append(inList, "'s'")
  534. }
  535. inList = append(inList, "'d'")
  536. return " AND ref.type IN (" + strings.Join(inList, ",") + ")"
  537. }
  538. func graphDailyNoteFilter(local bool) string {
  539. dailyNotesPaths := dailyNotePaths(local)
  540. if 1 > len(dailyNotesPaths) {
  541. return ""
  542. }
  543. buf := bytes.Buffer{}
  544. for _, p := range dailyNotesPaths {
  545. buf.WriteString(" AND ref.hpath NOT LIKE '" + p + "%'")
  546. }
  547. return buf.String()
  548. }
  549. func dailyNotePaths(local bool) (ret []string) {
  550. dailyNote := Conf.Graph.Local.DailyNote
  551. if !local {
  552. dailyNote = Conf.Graph.Global.DailyNote
  553. }
  554. if dailyNote {
  555. return
  556. }
  557. for _, box := range Conf.GetOpenedBoxes() {
  558. boxConf := box.GetConf()
  559. if 1 < strings.Count(boxConf.DailyNoteSavePath, "/") {
  560. dailyNoteSaveDir := strings.Split(boxConf.DailyNoteSavePath, "/")[1]
  561. ret = append(ret, "/"+dailyNoteSaveDir)
  562. }
  563. }
  564. ret = gulu.Str.RemoveDuplicatedElem(ret)
  565. return
  566. }
  567. func nodeTitleLabel(node *GraphNode, blockContent string) {
  568. if "NodeDocument" != node.Type && "NodeHeading" != node.Type {
  569. node.Title = blockContent
  570. } else {
  571. node.Label = blockContent
  572. }
  573. }
  574. func query2Stmt(queryStr string) (ret string) {
  575. buf := bytes.Buffer{}
  576. if ast.IsNodeIDPattern(queryStr) {
  577. buf.WriteString("id = '" + queryStr + "'")
  578. } else {
  579. var tags []string
  580. luteEngine := util.NewLute()
  581. t := parse.Inline("", []byte(queryStr), luteEngine.ParseOptions)
  582. ast.Walk(t.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  583. if !entering {
  584. return ast.WalkContinue
  585. }
  586. if n.IsTextMarkType("tag") {
  587. tags = append(tags, n.Text())
  588. }
  589. return ast.WalkContinue
  590. })
  591. for _, tag := range tags {
  592. queryStr = strings.ReplaceAll(queryStr, "#"+tag+"#", "")
  593. }
  594. parts := strings.Split(queryStr, " ")
  595. for i, part := range parts {
  596. if "" == part {
  597. continue
  598. }
  599. part = strings.ReplaceAll(part, "'", "''")
  600. buf.WriteString("(content LIKE '%" + part + "%'")
  601. buf.WriteString(Conf.Search.NAMFilter(part))
  602. buf.WriteString(")")
  603. if i < len(parts)-1 {
  604. buf.WriteString(" AND ")
  605. }
  606. }
  607. if 0 < len(tags) {
  608. if 0 < buf.Len() {
  609. buf.WriteString(" OR ")
  610. }
  611. for i, tag := range tags {
  612. buf.WriteString("(content LIKE '%#" + tag + "#%')")
  613. if i < len(tags)-1 {
  614. buf.WriteString(" AND ")
  615. }
  616. }
  617. buf.WriteString(" OR ")
  618. for i, tag := range tags {
  619. buf.WriteString("ial LIKE '%tags=\"%" + tag + "%\"%'")
  620. if i < len(tags)-1 {
  621. buf.WriteString(" AND ")
  622. }
  623. }
  624. }
  625. }
  626. if 1 > buf.Len() {
  627. buf.WriteString("1=1")
  628. }
  629. ret = buf.String()
  630. return
  631. }