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