A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 703 lines 17 kB view raw
1// SiYuan - Refactor your thinking 2// Copyright (c) 2020-present, b3log.org 3// 4// This program is free software: you can redistribute it and/or modify 5// it under the terms of the GNU Affero General Public License as published by 6// the Free Software Foundation, either version 3 of the License, or 7// (at your option) any later version. 8// 9// This program is distributed in the hope that it will be useful, 10// but WITHOUT ANY WARRANTY; without even the implied warranty of 11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12// GNU Affero General Public License for more details. 13// 14// You should have received a copy of the GNU Affero General Public License 15// along with this program. If not, see <https://www.gnu.org/licenses/>. 16 17package model 18 19import ( 20 "bytes" 21 "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}