A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 689 lines 18 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 "os" 21 "path/filepath" 22 "sort" 23 "strings" 24 "unicode/utf8" 25 26 "github.com/88250/gulu" 27 "github.com/88250/lute/ast" 28 "github.com/88250/lute/editor" 29 "github.com/88250/lute/parse" 30 "github.com/emirpasic/gods/sets/hashset" 31 "github.com/siyuan-note/logging" 32 "github.com/siyuan-note/siyuan/kernel/av" 33 "github.com/siyuan-note/siyuan/kernel/filesys" 34 "github.com/siyuan-note/siyuan/kernel/sql" 35 "github.com/siyuan-note/siyuan/kernel/treenode" 36 "github.com/siyuan-note/siyuan/kernel/util" 37) 38 39type BlockInfo struct { 40 ID string `json:"id"` 41 RootID string `json:"rootID"` 42 Name string `json:"name"` 43 RefCount int `json:"refCount"` 44 SubFileCount int `json:"subFileCount"` 45 RefIDs []string `json:"refIDs"` 46 IAL map[string]string `json:"ial"` 47 Icon string `json:"icon"` 48 AttrViews []*AttrView `json:"attrViews"` 49} 50 51type AttrView struct { 52 ID string `json:"id"` 53 Name string `json:"name"` 54} 55 56func GetDocInfo(blockID string) (ret *BlockInfo) { 57 FlushTxQueue() 58 59 tree, err := LoadTreeByBlockID(blockID) 60 if err != nil { 61 logging.LogErrorf("load tree by root id [%s] failed: %s", blockID, err) 62 return 63 } 64 65 title := tree.Root.IALAttr("title") 66 ret = &BlockInfo{ID: blockID, RootID: tree.Root.ID, Name: title} 67 ret.IAL = parse.IAL2Map(tree.Root.KramdownIAL) 68 scrollData := ret.IAL["scroll"] 69 if 0 < len(scrollData) { 70 scroll := map[string]interface{}{} 71 if parseErr := gulu.JSON.UnmarshalJSON([]byte(scrollData), &scroll); nil != parseErr { 72 logging.LogWarnf("parse scroll data [%s] failed: %s", scrollData, parseErr) 73 delete(ret.IAL, "scroll") 74 } else { 75 if zoomInId := scroll["zoomInId"]; nil != zoomInId { 76 if !treenode.ExistBlockTree(zoomInId.(string)) { 77 delete(ret.IAL, "scroll") 78 } 79 } else { 80 if startId := scroll["startId"]; nil != startId { 81 if !treenode.ExistBlockTree(startId.(string)) { 82 delete(ret.IAL, "scroll") 83 } 84 } 85 if endId := scroll["endId"]; nil != endId { 86 if !treenode.ExistBlockTree(endId.(string)) { 87 delete(ret.IAL, "scroll") 88 } 89 } 90 } 91 } 92 } 93 94 bt := treenode.GetBlockTree(blockID) 95 refDefs := queryBlockRefDefs(bt) 96 buildBacklinkListItemRefs(refDefs) 97 var refIDs []string 98 for _, refDef := range refDefs { 99 refIDs = append(refIDs, refDef.RefID) 100 } 101 if 1 > len(refIDs) { 102 refIDs = []string{} 103 } 104 ret.RefIDs = refIDs 105 ret.RefCount = len(ret.RefIDs) 106 107 // 填充属性视图角标 Display the database title on the block superscript https://github.com/siyuan-note/siyuan/issues/10545 108 avIDs := strings.Split(ret.IAL[av.NodeAttrNameAvs], ",") 109 for _, avID := range avIDs { 110 avName, getErr := av.GetAttributeViewName(avID) 111 if nil != getErr { 112 continue 113 } 114 115 if "" == avName { 116 avName = Conf.language(105) 117 } 118 119 attrView := &AttrView{ID: avID, Name: avName} 120 ret.AttrViews = append(ret.AttrViews, attrView) 121 } 122 123 var subFileCount int 124 boxLocalPath := filepath.Join(util.DataDir, tree.Box) 125 subFiles, err := os.ReadDir(filepath.Join(boxLocalPath, strings.TrimSuffix(tree.Path, ".sy"))) 126 if err == nil { 127 for _, subFile := range subFiles { 128 if strings.HasSuffix(subFile.Name(), ".sy") { 129 subFileCount++ 130 } 131 } 132 } 133 ret.SubFileCount = subFileCount 134 ret.Icon = tree.Root.IALAttr("icon") 135 return 136} 137 138func GetDocsInfo(blockIDs []string, queryRefCount bool, queryAv bool) (rets []*BlockInfo) { 139 FlushTxQueue() 140 141 trees := filesys.LoadTrees(blockIDs) 142 bts := treenode.GetBlockTrees(blockIDs) 143 for _, blockID := range blockIDs { 144 tree := trees[blockID] 145 if nil == tree { 146 continue 147 } 148 title := tree.Root.IALAttr("title") 149 ret := &BlockInfo{ID: blockID, RootID: tree.Root.ID, Name: title} 150 ret.IAL = parse.IAL2Map(tree.Root.KramdownIAL) 151 scrollData := ret.IAL["scroll"] 152 if 0 < len(scrollData) { 153 scroll := map[string]interface{}{} 154 if parseErr := gulu.JSON.UnmarshalJSON([]byte(scrollData), &scroll); nil != parseErr { 155 logging.LogWarnf("parse scroll data [%s] failed: %s", scrollData, parseErr) 156 delete(ret.IAL, "scroll") 157 } else { 158 if zoomInId := scroll["zoomInId"]; nil != zoomInId { 159 if !treenode.ExistBlockTree(zoomInId.(string)) { 160 delete(ret.IAL, "scroll") 161 } 162 } else { 163 if startId := scroll["startId"]; nil != startId { 164 if !treenode.ExistBlockTree(startId.(string)) { 165 delete(ret.IAL, "scroll") 166 } 167 } 168 if endId := scroll["endId"]; nil != endId { 169 if !treenode.ExistBlockTree(endId.(string)) { 170 delete(ret.IAL, "scroll") 171 } 172 } 173 } 174 } 175 } 176 if queryRefCount { 177 var refIDs []string 178 refDefs := queryBlockRefDefs(bts[blockID]) 179 buildBacklinkListItemRefs(refDefs) 180 for _, refDef := range refDefs { 181 refIDs = append(refIDs, refDef.RefID) 182 } 183 if 1 > len(refIDs) { 184 refIDs = []string{} 185 } 186 ret.RefIDs = refIDs 187 ret.RefCount = len(ret.RefIDs) 188 } 189 190 if queryAv { 191 // 填充属性视图角标 Display the database title on the block superscript https://github.com/siyuan-note/siyuan/issues/10545 192 avIDs := strings.Split(ret.IAL[av.NodeAttrNameAvs], ",") 193 for _, avID := range avIDs { 194 avName, getErr := av.GetAttributeViewName(avID) 195 if nil != getErr { 196 continue 197 } 198 199 if "" == avName { 200 avName = Conf.language(105) 201 } 202 203 attrView := &AttrView{ID: avID, Name: avName} 204 ret.AttrViews = append(ret.AttrViews, attrView) 205 } 206 } 207 208 var subFileCount int 209 boxLocalPath := filepath.Join(util.DataDir, tree.Box) 210 subFiles, err := os.ReadDir(filepath.Join(boxLocalPath, strings.TrimSuffix(tree.Path, ".sy"))) 211 if err == nil { 212 for _, subFile := range subFiles { 213 if strings.HasSuffix(subFile.Name(), ".sy") { 214 subFileCount++ 215 } 216 } 217 } 218 ret.SubFileCount = subFileCount 219 ret.Icon = tree.Root.IALAttr("icon") 220 221 rets = append(rets, ret) 222 223 } 224 return 225} 226 227func GetBlockRefText(id string) string { 228 FlushTxQueue() 229 230 bt := treenode.GetBlockTree(id) 231 if nil == bt { 232 return ErrBlockNotFound.Error() 233 } 234 235 tree, err := LoadTreeByBlockID(id) 236 if err != nil { 237 return "" 238 } 239 240 node := treenode.GetNodeInTree(tree, id) 241 if nil == node { 242 return ErrBlockNotFound.Error() 243 } 244 245 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus { 246 if !entering { 247 return ast.WalkContinue 248 } 249 250 if n.IsTextMarkType("inline-memo") { 251 // Block ref anchor text no longer contains contents of inline-level memos https://github.com/siyuan-note/siyuan/issues/9363 252 n.TextMarkInlineMemoContent = "" 253 return ast.WalkContinue 254 } 255 return ast.WalkContinue 256 }) 257 return getNodeRefText(node) 258} 259 260func GetDOMText(dom string) (ret string) { 261 luteEngine := NewLute() 262 tree := luteEngine.BlockDOM2Tree(dom) 263 ret = renderBlockText(tree.Root.FirstChild, nil, true) 264 return 265} 266 267func getBlockRefText(id string, tree *parse.Tree) (ret string) { 268 node := treenode.GetNodeInTree(tree, id) 269 if nil == node { 270 return 271 } 272 273 ret = getNodeRefText(node) 274 ret = maxContent(ret, Conf.Editor.BlockRefDynamicAnchorTextMaxLen) 275 return 276} 277 278func getNodeRefText(node *ast.Node) string { 279 if nil == node { 280 return "" 281 } 282 283 if ret := node.IALAttr("name"); "" != ret { 284 ret = strings.TrimSpace(ret) 285 ret = util.EscapeHTML(ret) 286 return ret 287 } 288 return getNodeRefText0(node, Conf.Editor.BlockRefDynamicAnchorTextMaxLen, true) 289} 290 291func getNodeAvBlockText(node *ast.Node, avID string) (icon, content string) { 292 if nil == node { 293 return 294 } 295 296 icon = node.IALAttr("icon") 297 if name := node.IALAttr("name"); "" != name { 298 name = strings.TrimSpace(name) 299 name = util.EscapeHTML(name) 300 content = name 301 } else { 302 content = getNodeRefText0(node, 1024, false) 303 } 304 305 content = strings.TrimSpace(content) 306 if "" != avID { 307 if staticText := node.IALAttr(av.NodeAttrViewStaticText + "-" + avID); "" != staticText { 308 content = staticText 309 } 310 } 311 if "" == content { 312 content = Conf.language(105) 313 } 314 return 315} 316 317func getNodeRefText0(node *ast.Node, maxLen int, removeLineBreak bool) string { 318 switch node.Type { 319 case ast.NodeBlockQueryEmbed: 320 return "Query Embed Block..." 321 case ast.NodeIFrame: 322 return "IFrame..." 323 case ast.NodeThematicBreak: 324 return "Thematic Break..." 325 case ast.NodeVideo: 326 return "Video..." 327 case ast.NodeAudio: 328 return "Audio..." 329 case ast.NodeAttributeView: 330 ret, _ := av.GetAttributeViewName(node.AttributeViewID) 331 if "" == ret { 332 ret = "Database..." 333 } 334 return ret 335 } 336 337 if ast.NodeDocument != node.Type && node.IsContainerBlock() { 338 node = treenode.FirstLeafBlock(node) 339 } 340 ret := renderBlockText(node, nil, removeLineBreak) 341 if maxLen < utf8.RuneCountInString(ret) { 342 ret = gulu.Str.SubStr(ret, maxLen) + "..." 343 } 344 return ret 345} 346 347type RefDefs struct { 348 RefID string `json:"refID"` 349 DefIDs []string `json:"defIDs"` 350} 351 352func GetBlockRefs(defID string) (refDefs []*RefDefs, originalRefBlockIDs map[string]string) { 353 refDefs = []*RefDefs{} 354 originalRefBlockIDs = map[string]string{} 355 bt := treenode.GetBlockTree(defID) 356 if nil == bt { 357 return 358 } 359 360 refDefs = queryBlockRefDefs(bt) 361 originalRefBlockIDs = buildBacklinkListItemRefs(refDefs) 362 return 363} 364 365func queryBlockRefDefs(bt *treenode.BlockTree) (refDefs []*RefDefs) { 366 refDefs = []*RefDefs{} 367 if nil == bt { 368 return 369 } 370 371 isDoc := bt.ID == bt.RootID 372 if isDoc { 373 refDefIDs := sql.QueryChildRefDefIDsByRootDefID(bt.RootID) 374 for rID, dIDs := range refDefIDs { 375 var defIDs []string 376 for _, dID := range dIDs { 377 defIDs = append(defIDs, dID) 378 } 379 if 1 > len(defIDs) { 380 defIDs = []string{} 381 } 382 refDefs = append(refDefs, &RefDefs{RefID: rID, DefIDs: defIDs}) 383 } 384 } else { 385 refIDs := sql.QueryRefIDsByDefID(bt.ID, false) 386 for _, refID := range refIDs { 387 refDefs = append(refDefs, &RefDefs{RefID: refID, DefIDs: []string{bt.ID}}) 388 } 389 } 390 return 391} 392 393func GetBlockRefIDsByFileAnnotationID(id string) []string { 394 return sql.QueryRefIDsByAnnotationID(id) 395} 396 397func GetBlockDefIDsByRefText(refText string, excludeIDs []string) (ret []string) { 398 ret = sql.QueryBlockDefIDsByRefText(refText, excludeIDs) 399 sort.Sort(sort.Reverse(sort.StringSlice(ret))) 400 if 1 > len(ret) { 401 ret = []string{} 402 } 403 return 404} 405 406func GetBlockIndex(id string) (ret int) { 407 tree, _ := LoadTreeByBlockID(id) 408 if nil == tree { 409 return 410 } 411 node := treenode.GetNodeInTree(tree, id) 412 if nil == node { 413 return 414 } 415 416 rootChild := node 417 for ; nil != rootChild.Parent && ast.NodeDocument != rootChild.Parent.Type; rootChild = rootChild.Parent { 418 } 419 420 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 421 if !entering { 422 return ast.WalkContinue 423 } 424 425 if !n.IsChildBlockOf(tree.Root, 1) { 426 return ast.WalkContinue 427 } 428 429 ret++ 430 if n.ID == rootChild.ID { 431 return ast.WalkStop 432 } 433 return ast.WalkContinue 434 }) 435 return 436} 437 438func GetBlocksIndexes(ids []string) (ret map[string]int) { 439 ret = map[string]int{} 440 if 1 > len(ids) { 441 return 442 } 443 444 tree, _ := LoadTreeByBlockID(ids[0]) 445 if nil == tree { 446 return 447 } 448 449 idx := 0 450 nodesIndexes := map[string]int{} 451 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 452 if !entering { 453 return ast.WalkContinue 454 } 455 456 if !n.IsChildBlockOf(tree.Root, 1) { 457 if n.IsBlock() { 458 nodesIndexes[n.ID] = idx 459 } 460 return ast.WalkContinue 461 } 462 463 idx++ 464 nodesIndexes[n.ID] = idx 465 return ast.WalkContinue 466 }) 467 468 for _, id := range ids { 469 ret[id] = nodesIndexes[id] 470 } 471 return 472} 473 474type BlockPath struct { 475 ID string `json:"id"` 476 Name string `json:"name"` 477 Type string `json:"type"` 478 SubType string `json:"subType"` 479 Children []*BlockPath `json:"children"` 480} 481 482func BuildBlockBreadcrumb(id string, excludeTypes []string) (ret []*BlockPath, err error) { 483 ret = []*BlockPath{} 484 tree, err := LoadTreeByBlockID(id) 485 if nil == tree { 486 err = nil 487 return 488 } 489 node := treenode.GetNodeInTree(tree, id) 490 if nil == node { 491 return 492 } 493 494 ret = buildBlockBreadcrumb(node, excludeTypes, false) 495 return 496} 497 498func buildBlockBreadcrumb(node *ast.Node, excludeTypes []string, isEmbedBlock bool, headingMode ...int) (ret []*BlockPath) { 499 ret = []*BlockPath{} 500 if nil == node { 501 return 502 } 503 box := Conf.Box(node.Box) 504 if nil == box { 505 return 506 } 507 508 // 默认 headingMode 为 0 509 mode := 0 510 if len(headingMode) > 0 { 511 mode = headingMode[0] 512 } 513 514 headingLevel := 16 515 maxNameLen := 1024 516 var hPath string 517 baseBlock := treenode.GetBlockTreeRootByPath(node.Box, node.Path) 518 if nil != baseBlock { 519 hPath = baseBlock.HPath 520 } 521 for parent := node; nil != parent; parent = parent.Parent { 522 if "" == parent.ID { 523 continue 524 } 525 id := parent.ID 526 fc := treenode.FirstLeafBlock(parent) 527 528 name := parent.IALAttr("name") 529 if ast.NodeDocument == parent.Type { 530 name = box.Name + hPath 531 } else if ast.NodeAttributeView == parent.Type { 532 name, _ = av.GetAttributeViewName(parent.AttributeViewID) 533 } else { 534 if "" == name { 535 if ast.NodeListItem == parent.Type || ast.NodeList == parent.Type || ast.NodeSuperBlock == parent.Type || ast.NodeBlockquote == parent.Type { 536 name = gulu.Str.SubStr(renderBlockText(fc, excludeTypes, true), maxNameLen) 537 } else { 538 name = gulu.Str.SubStr(renderBlockText(parent, excludeTypes, true), maxNameLen) 539 } 540 } 541 if ast.NodeHeading == parent.Type { 542 headingLevel = parent.HeadingLevel 543 } 544 } 545 546 add := true 547 if ast.NodeList == parent.Type || ast.NodeSuperBlock == parent.Type || ast.NodeBlockquote == parent.Type { 548 add = false 549 if parent == node { 550 // https://github.com/siyuan-note/siyuan/issues/13141#issuecomment-2476789553 551 add = true 552 } 553 } 554 if ast.NodeParagraph == parent.Type && nil != parent.Parent && ast.NodeListItem == parent.Parent.Type && nil == parent.Next && (nil == parent.Previous || ast.NodeTaskListItemMarker == parent.Previous.Type) { 555 add = false 556 } 557 if ast.NodeListItem == parent.Type { 558 if "" == name { 559 name = gulu.Str.SubStr(renderBlockText(fc, excludeTypes, true), maxNameLen) 560 } 561 } 562 563 name = strings.ReplaceAll(name, editor.Caret, "") 564 name = util.UnescapeHTML(name) 565 name = util.EscapeHTML(name) 566 567 if !isEmbedBlock { 568 if parent == node { 569 name = "" 570 } 571 } else { 572 if ast.NodeDocument != parent.Type { 573 // 当headingMode=2(仅显示标题下方的块)且当前节点是标题时,保留标题名称 574 if 2 == mode && ast.NodeHeading == parent.Type && parent == node { 575 // 保留标题名称,不清空 576 } else { 577 // 在嵌入块中隐藏最后一个非文档路径的面包屑中的文本 Hide text in breadcrumb of last non-document path in embed block https://github.com/siyuan-note/siyuan/issues/13866 578 name = "" 579 } 580 } 581 } 582 583 if add { 584 ret = append([]*BlockPath{{ 585 ID: id, 586 Name: name, 587 Type: parent.Type.String(), 588 SubType: treenode.SubTypeAbbr(parent), 589 }}, ret...) 590 } 591 592 for prev := parent.Previous; nil != prev; prev = prev.Previous { 593 b := prev 594 if ast.NodeSuperBlock == prev.Type { 595 // 超级块中包含标题块时下方块面包屑计算不正确 https://github.com/siyuan-note/siyuan/issues/6675 596 b = treenode.SuperBlockLastHeading(prev) 597 if nil == b { 598 // 超级块下方块被作为嵌入块时设置显示面包屑后不渲染 https://github.com/siyuan-note/siyuan/issues/6690 599 b = prev 600 } 601 } 602 603 if ast.NodeHeading == b.Type && headingLevel > b.HeadingLevel { 604 if b.ParentIs(ast.NodeListItem) { 605 // 标题在列表下时不显示 https://github.com/siyuan-note/siyuan/issues/13008 606 continue 607 } 608 609 name = gulu.Str.SubStr(renderBlockText(b, excludeTypes, true), maxNameLen) 610 name = util.UnescapeHTML(name) 611 name = util.EscapeHTML(name) 612 ret = append([]*BlockPath{{ 613 ID: b.ID, 614 Name: name, 615 Type: b.Type.String(), 616 SubType: treenode.SubTypeAbbr(b), 617 }}, ret...) 618 headingLevel = b.HeadingLevel 619 } 620 } 621 } 622 return 623} 624 625func buildBacklinkListItemRefs(refDefs []*RefDefs) (originalRefBlockIDs map[string]string) { 626 originalRefBlockIDs = map[string]string{} 627 628 var refIDs []string 629 for _, refDef := range refDefs { 630 refIDs = append(refIDs, refDef.RefID) 631 } 632 sqlRefBlocks := sql.GetBlocks(refIDs) 633 refBlocks := fromSQLBlocks(&sqlRefBlocks, "", 12) 634 635 parentRefParagraphs := map[string]*Block{} 636 var paragraphParentIDs []string 637 for _, ref := range refBlocks { 638 if nil != ref && "NodeParagraph" == ref.Type { 639 parentRefParagraphs[ref.ParentID] = ref 640 paragraphParentIDs = append(paragraphParentIDs, ref.ParentID) 641 } 642 } 643 sqlParagraphParents := sql.GetBlocks(paragraphParentIDs) 644 paragraphParents := fromSQLBlocks(&sqlParagraphParents, "", 12) 645 646 luteEngine := util.NewLute() 647 processedParagraphs := hashset.New() 648 for _, parent := range paragraphParents { 649 if nil == parent { 650 continue 651 } 652 653 if "NodeListItem" == parent.Type || "NodeBlockquote" == parent.Type || "NodeSuperBlock" == parent.Type { 654 refBlock := parentRefParagraphs[parent.ID] 655 if nil == refBlock { 656 continue 657 } 658 659 paragraphUseParentLi := true 660 if "NodeListItem" == parent.Type && parent.FContent != refBlock.Content { 661 if inlineTree := parse.Inline("", []byte(refBlock.Markdown), luteEngine.ParseOptions); nil != inlineTree { 662 for c := inlineTree.Root.FirstChild.FirstChild; c != nil; c = c.Next { 663 if treenode.IsBlockRef(c) { 664 continue 665 } 666 667 if "" != strings.TrimSpace(c.Text()) { 668 paragraphUseParentLi = false 669 break 670 } 671 } 672 } 673 } 674 675 if paragraphUseParentLi { 676 for _, refDef := range refDefs { 677 if refDef.RefID == refBlock.ID { 678 refDef.RefID = parent.ID 679 break 680 } 681 } 682 processedParagraphs.Add(parent.ID) 683 } 684 685 originalRefBlockIDs[parent.ID] = refBlock.ID 686 } 687 } 688 return 689}