A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 1242 lines 31 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 "errors" 22 "fmt" 23 "html" 24 "regexp" 25 "slices" 26 "strings" 27 "time" 28 29 "github.com/88250/gulu" 30 "github.com/88250/lute" 31 "github.com/88250/lute/ast" 32 "github.com/88250/lute/editor" 33 "github.com/88250/lute/parse" 34 "github.com/88250/lute/render" 35 "github.com/open-spaced-repetition/go-fsrs/v3" 36 "github.com/siyuan-note/siyuan/kernel/filesys" 37 "github.com/siyuan-note/siyuan/kernel/sql" 38 "github.com/siyuan-note/siyuan/kernel/treenode" 39 "github.com/siyuan-note/siyuan/kernel/util" 40) 41 42// Block 描述了内容块。 43type Block struct { 44 Box string `json:"box"` 45 Path string `json:"path"` 46 HPath string `json:"hPath"` 47 ID string `json:"id"` 48 RootID string `json:"rootID"` 49 ParentID string `json:"parentID"` 50 Name string `json:"name"` 51 Alias string `json:"alias"` 52 Memo string `json:"memo"` 53 Tag string `json:"tag"` 54 Content string `json:"content"` 55 FContent string `json:"fcontent"` 56 Markdown string `json:"markdown"` 57 Folded bool `json:"folded"` 58 Type string `json:"type"` 59 SubType string `json:"subType"` 60 RefText string `json:"refText"` 61 Defs []*Block `json:"-"` // 当前块引用了这些块,避免序列化 JSON 时产生循环引用 62 Refs []*Block `json:"refs"` // 当前块被这些块引用 63 DefID string `json:"defID"` 64 DefPath string `json:"defPath"` 65 IAL map[string]string `json:"ial"` 66 Children []*Block `json:"children"` 67 Depth int `json:"depth"` 68 Count int `json:"count"` 69 RefCount int `json:"refCount"` 70 Sort int `json:"sort"` 71 Created string `json:"created"` 72 Updated string `json:"updated"` 73 74 RiffCardID string `json:"riffCardID"` 75 RiffCard *RiffCard `json:"riffCard"` 76} 77 78type RiffCard struct { 79 Due time.Time `json:"due"` 80 Reps uint64 `json:"reps"` 81 Lapses uint64 `json:"lapses"` 82 State fsrs.State `json:"state"` 83 LastReview time.Time `json:"lastReview"` 84} 85 86func (block *Block) IsContainerBlock() bool { 87 switch block.Type { 88 case "NodeDocument", "NodeBlockquote", "NodeList", "NodeListItem", "NodeSuperBlock": 89 return true 90 } 91 return false 92} 93 94func (block *Block) IsDoc() bool { 95 return "NodeDocument" == block.Type 96} 97 98type Path struct { 99 ID string `json:"id"` // 块 ID 100 Box string `json:"box"` // 块 Box 101 Name string `json:"name"` // 当前路径 102 HPath string `json:"hPath"` // 人类可读路径 103 Type string `json:"type"` // "path" 104 NodeType string `json:"nodeType"` // 节点类型 105 SubType string `json:"subType"` // 节点子类型 106 Blocks []*Block `json:"blocks,omitempty"` // 子块节点 107 Children []*Path `json:"children,omitempty"` // 子路径节点 108 Depth int `json:"depth"` // 层级深度 109 Count int `json:"count"` // 子块计数 110 Folded bool `json:"folded"` // 是否折叠 111 112 Updated string `json:"updated"` // 更新时间 113 Created string `json:"created"` // 创建时间 114} 115 116func CheckBlockRef(ids []string) bool { 117 bts := treenode.GetBlockTrees(ids) 118 119 var rootIDs, blockIDs []string 120 for _, bt := range bts { 121 if "d" == bt.Type { 122 rootIDs = append(rootIDs, bt.ID) 123 } else { 124 blockIDs = append(blockIDs, bt.ID) 125 } 126 } 127 rootIDs = gulu.Str.RemoveDuplicatedElem(rootIDs) 128 blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs) 129 130 existRef := func(refCounts map[string]int) bool { 131 for _, refCount := range refCounts { 132 if 0 < refCount { 133 return true 134 } 135 } 136 return false 137 } 138 139 for _, rootID := range rootIDs { 140 refCounts := sql.QueryRootChildrenRefCount(rootID) 141 if existRef(refCounts) { 142 return true 143 } 144 } 145 146 refCounts := sql.QueryRefCount(blockIDs) 147 if existRef(refCounts) { 148 return true 149 } 150 151 // TODO 还需要考虑容器块的子块引用计数 https://github.com/siyuan-note/siyuan/issues/13396 152 153 return false 154} 155 156type BlockTreeInfo struct { 157 ID string `json:"id"` 158 Type string `json:"type"` 159 ParentID string `json:"parentID"` 160 ParentType string `json:"parentType"` 161 PreviousID string `json:"previousID"` 162 PreviousType string `json:"previousType"` 163 NextID string `json:"nextID"` 164 NextType string `json:"nextType"` 165} 166 167func GetBlockTreeInfos(ids []string) (ret map[string]*BlockTreeInfo) { 168 ret = map[string]*BlockTreeInfo{} 169 trees := filesys.LoadTrees(ids) 170 for id, tree := range trees { 171 node := treenode.GetNodeInTree(tree, id) 172 if nil == node { 173 ret[id] = &BlockTreeInfo{ID: id} 174 continue 175 } 176 177 bti := &BlockTreeInfo{ID: id, Type: node.Type.String()} 178 ret[id] = bti 179 parent := treenode.ParentBlock(node) 180 if nil != parent { 181 bti.ParentID = parent.ID 182 bti.ParentType = parent.Type.String() 183 } 184 previous := treenode.PreviousBlock(node) 185 if nil != previous { 186 bti.PreviousID = previous.ID 187 bti.PreviousType = previous.Type.String() 188 } 189 next := treenode.NextBlock(node) 190 if nil != next { 191 bti.NextID = next.ID 192 bti.NextType = next.Type.String() 193 } 194 } 195 return 196} 197 198func GetBlockSiblingID(id string) (parent, previous, next string) { 199 tree, err := LoadTreeByBlockID(id) 200 if err != nil { 201 return 202 } 203 204 current := treenode.GetNodeInTree(tree, id) 205 if nil == current || !current.IsBlock() { 206 return 207 } 208 parentBlock := treenode.ParentBlock(current) 209 if nil == parentBlock { 210 return 211 } 212 213 parent = parentBlock.ID 214 if ast.NodeDocument == parentBlock.Type { 215 parent = parentBlock.ID 216 217 if nil != current.Previous && current.Previous.IsBlock() { 218 previous = current.Previous.ID 219 if flb := treenode.FirstChildBlock(current.Previous); nil != flb { 220 previous = flb.ID 221 } 222 } 223 224 if nil != current.Next && current.Next.IsBlock() { 225 next = current.Next.ID 226 if flb := treenode.FirstChildBlock(current.Next); nil != flb { 227 next = flb.ID 228 } 229 } 230 return 231 } 232 233 for ; nil != parentBlock; parentBlock = treenode.ParentBlock(parentBlock) { 234 if nil != parentBlock.Previous && parentBlock.Previous.IsBlock() { 235 previous = parentBlock.Previous.ID 236 if flb := treenode.FirstChildBlock(parentBlock.Previous); nil != flb { 237 previous = flb.ID 238 } 239 break 240 } 241 } 242 parentBlock = treenode.ParentBlock(current) 243 for ; nil != parentBlock; parentBlock = treenode.ParentBlock(parentBlock) { 244 if nil != parentBlock.Next && parentBlock.Next.IsBlock() { 245 next = parentBlock.Next.ID 246 if flb := treenode.FirstChildBlock(parentBlock.Next); nil != flb { 247 next = flb.ID 248 } 249 break 250 } 251 } 252 return 253} 254 255func GetBlockRelevantIDs(id string) (parentID, previousID, nextID string, err error) { 256 tree, err := LoadTreeByBlockID(id) 257 if err != nil { 258 return 259 } 260 261 node := treenode.GetNodeInTree(tree, id) 262 if nil == node { 263 err = ErrBlockNotFound 264 return 265 } 266 267 if nil != node.Parent { 268 parentID = node.Parent.ID 269 } 270 if nil != node.Previous { 271 previous := node.Previous 272 if ast.NodeKramdownBlockIAL == previous.Type { 273 previous = previous.Previous 274 } 275 if nil != previous { 276 previousID = previous.ID 277 } 278 } 279 if nil != node.Next { 280 next := node.Next 281 if ast.NodeKramdownBlockIAL == next.Type { 282 next = next.Next 283 } 284 if nil != next { 285 nextID = next.ID 286 } 287 } 288 return 289} 290 291func GetUnfoldedParentID(id string) (parentID string) { 292 tree, err := LoadTreeByBlockID(id) 293 if err != nil { 294 return 295 } 296 297 node := treenode.GetNodeInTree(tree, id) 298 if nil == node { 299 return 300 } 301 302 if !node.IsBlock() { 303 return 304 } 305 306 var firstFoldedParent *ast.Node 307 for parent := treenode.HeadingParent(node); nil != parent && ast.NodeDocument != parent.Type; parent = treenode.HeadingParent(parent) { 308 if "1" == parent.IALAttr("fold") { 309 firstFoldedParent = parent 310 parentID = firstFoldedParent.ID 311 } else { 312 if nil != firstFoldedParent { 313 parentID = firstFoldedParent.ID 314 } else { 315 parentID = id 316 } 317 return 318 } 319 } 320 if "" == parentID { 321 parentID = id 322 } 323 return 324} 325 326func IsBlockFolded(id string) (isFolded, isRoot bool) { 327 tree, _ := LoadTreeByBlockID(id) 328 if nil == tree { 329 return 330 } 331 332 if tree.Root.ID == id { 333 isRoot = true 334 } 335 336 for i := 0; i < 32; i++ { 337 b, _ := getBlock(id, nil) 338 if nil == b { 339 return 340 } 341 342 if "1" == b.IAL["fold"] { 343 isFolded = true 344 return 345 } 346 347 id = b.ParentID 348 349 } 350 return 351} 352 353func RecentUpdatedBlocks() (ret []*Block) { 354 ret = []*Block{} 355 356 sqlStmt := "SELECT * FROM blocks WHERE type = 'p' AND length > 1" 357 if util.ContainerIOS == util.Container || util.ContainerAndroid == util.Container || util.ContainerHarmony == util.Container { 358 sqlStmt = "SELECT * FROM blocks WHERE type = 'd'" 359 } 360 361 if ignoreLines := getSearchIgnoreLines(); 0 < len(ignoreLines) { 362 // Support ignore search results https://github.com/siyuan-note/siyuan/issues/10089 363 buf := bytes.Buffer{} 364 for _, line := range ignoreLines { 365 buf.WriteString(" AND ") 366 buf.WriteString(line) 367 } 368 sqlStmt += buf.String() 369 } 370 371 sqlStmt += " ORDER BY updated DESC" 372 sqlBlocks := sql.SelectBlocksRawStmt(sqlStmt, 1, 16) 373 if 1 > len(sqlBlocks) { 374 return 375 } 376 377 ret = fromSQLBlocks(&sqlBlocks, "", 0) 378 return 379} 380 381func TransferBlockRef(fromID, toID string, refIDs []string) (err error) { 382 toTree, _ := LoadTreeByBlockID(toID) 383 if nil == toTree { 384 err = ErrBlockNotFound 385 return 386 } 387 toNode := treenode.GetNodeInTree(toTree, toID) 388 if nil == toNode { 389 err = ErrBlockNotFound 390 return 391 } 392 toRefText := getNodeRefText(toNode) 393 394 util.PushMsg(Conf.Language(116), 7000) 395 396 if 1 > len(refIDs) { // 如果不指定 refIDs,则转移所有引用了 fromID 的块 397 refIDs = sql.QueryRefIDsByDefID(fromID, false) 398 } 399 400 trees := filesys.LoadTrees(refIDs) 401 for refID, tree := range trees { 402 if nil == tree { 403 continue 404 } 405 406 node := treenode.GetNodeInTree(tree, refID) 407 textMarks := node.ChildrenByType(ast.NodeTextMark) 408 for _, textMark := range textMarks { 409 if textMark.IsTextMarkType("block-ref") && textMark.TextMarkBlockRefID == fromID { 410 textMark.TextMarkBlockRefID = toID 411 if "d" == textMark.TextMarkBlockRefSubtype { 412 textMark.TextMarkTextContent = toRefText 413 } 414 } 415 } 416 417 if err = indexWriteTreeUpsertQueue(tree); err != nil { 418 return 419 } 420 } 421 422 sql.FlushQueue() 423 return 424} 425 426func SwapBlockRef(refID, defID string, includeChildren bool) (err error) { 427 refTree, err := LoadTreeByBlockID(refID) 428 if err != nil { 429 return 430 } 431 refNode := treenode.GetNodeInTree(refTree, refID) 432 if nil == refNode { 433 return 434 } 435 if ast.NodeListItem == refNode.Parent.Type { 436 refNode = refNode.Parent 437 } 438 defTree, err := LoadTreeByBlockID(defID) 439 if err != nil { 440 return 441 } 442 sameTree := defTree.ID == refTree.ID 443 var defNode *ast.Node 444 if !sameTree { 445 defNode = treenode.GetNodeInTree(defTree, defID) 446 } else { 447 defNode = treenode.GetNodeInTree(refTree, defID) 448 } 449 if nil == defNode { 450 return 451 } 452 var defNodeChildren []*ast.Node 453 if ast.NodeListItem == defNode.Parent.Type { 454 defNode = defNode.Parent 455 } else if ast.NodeHeading == defNode.Type && includeChildren { 456 defNodeChildren = treenode.HeadingChildren(defNode) 457 } 458 if ast.NodeListItem == defNode.Type { 459 for c := defNode.FirstChild; nil != c; c = c.Next { 460 if ast.NodeList == c.Type { 461 defNodeChildren = append(defNodeChildren, c) 462 } 463 } 464 } 465 466 refreshUpdated(defNode) 467 refreshUpdated(refNode) 468 469 refPivot := treenode.NewParagraph("") 470 refNode.InsertBefore(refPivot) 471 472 if ast.NodeListItem == defNode.Type { 473 if ast.NodeListItem == refNode.Type { 474 if !includeChildren { 475 for _, c := range defNodeChildren { 476 refNode.AppendChild(c) 477 } 478 } 479 defNode.InsertAfter(refNode) 480 refPivot.InsertAfter(defNode) 481 } else { 482 newID := ast.NewNodeID() 483 li := &ast.Node{ID: newID, Type: ast.NodeListItem, ListData: &ast.ListData{Typ: defNode.Parent.ListData.Typ}} 484 li.SetIALAttr("id", newID) 485 li.SetIALAttr("updated", newID[:14]) 486 li.AppendChild(refNode) 487 defNode.InsertAfter(li) 488 if !includeChildren { 489 for _, c := range defNodeChildren { 490 li.AppendChild(c) 491 } 492 } 493 494 newID = ast.NewNodeID() 495 list := &ast.Node{ID: newID, Type: ast.NodeList, ListData: &ast.ListData{Typ: defNode.Parent.ListData.Typ}} 496 list.SetIALAttr("id", newID) 497 list.SetIALAttr("updated", newID[:14]) 498 list.AppendChild(defNode) 499 refPivot.InsertAfter(list) 500 } 501 } else { 502 if ast.NodeListItem == refNode.Type { 503 newID := ast.NewNodeID() 504 list := &ast.Node{ID: newID, Type: ast.NodeList, ListData: &ast.ListData{Typ: refNode.Parent.ListData.Typ}} 505 list.SetIALAttr("id", newID) 506 list.SetIALAttr("updated", newID[:14]) 507 list.AppendChild(refNode) 508 defNode.InsertAfter(list) 509 510 newID = ast.NewNodeID() 511 li := &ast.Node{ID: newID, Type: ast.NodeListItem, ListData: &ast.ListData{Typ: refNode.Parent.ListData.Typ}} 512 li.SetIALAttr("id", newID) 513 li.SetIALAttr("updated", newID[:14]) 514 li.AppendChild(defNode) 515 for i := len(defNodeChildren) - 1; -1 < i; i-- { 516 defNode.InsertAfter(defNodeChildren[i]) 517 } 518 refPivot.InsertAfter(li) 519 } else { 520 defNode.InsertAfter(refNode) 521 refPivot.InsertAfter(defNode) 522 for i := len(defNodeChildren) - 1; -1 < i; i-- { 523 defNode.InsertAfter(defNodeChildren[i]) 524 } 525 } 526 } 527 refPivot.Unlink() 528 529 if err = indexWriteTreeUpsertQueue(refTree); err != nil { 530 return 531 } 532 if !sameTree { 533 if err = indexWriteTreeUpsertQueue(defTree); err != nil { 534 return 535 } 536 } 537 FlushTxQueue() 538 util.ReloadUI() 539 return 540} 541 542func GetHeadingDeleteTransaction(id string) (transaction *Transaction, err error) { 543 tree, err := LoadTreeByBlockID(id) 544 if err != nil { 545 return 546 } 547 548 node := treenode.GetNodeInTree(tree, id) 549 if nil == node { 550 err = errors.New(fmt.Sprintf(Conf.Language(15), id)) 551 return 552 } 553 554 if ast.NodeHeading != node.Type { 555 return 556 } 557 558 var nodes []*ast.Node 559 nodes = append(nodes, node) 560 nodes = append(nodes, treenode.HeadingChildren(node)...) 561 562 transaction = &Transaction{} 563 luteEngine := util.NewLute() 564 for _, n := range nodes { 565 op := &Operation{} 566 op.ID = n.ID 567 op.Action = "delete" 568 transaction.DoOperations = append(transaction.DoOperations, op) 569 570 op = &Operation{} 571 op.ID = n.ID 572 if nil != n.Parent { 573 op.ParentID = n.Parent.ID 574 } 575 if nil != n.Previous { 576 op.PreviousID = n.Previous.ID 577 } 578 op.Action = "insert" 579 op.Data = luteEngine.RenderNodeBlockDOM(n) 580 transaction.UndoOperations = append(transaction.UndoOperations, op) 581 } 582 return 583} 584 585func GetHeadingInsertTransaction(id string) (transaction *Transaction, err error) { 586 tree, err := LoadTreeByBlockID(id) 587 if err != nil { 588 return 589 } 590 591 node := treenode.GetNodeInTree(tree, id) 592 if nil == node { 593 err = errors.New(fmt.Sprintf(Conf.Language(15), id)) 594 return 595 } 596 597 if ast.NodeHeading != node.Type { 598 return 599 } 600 601 var nodes []*ast.Node 602 nodes = append(nodes, node) 603 nodes = append(nodes, treenode.HeadingChildren(node)...) 604 605 transaction = &Transaction{} 606 luteEngine := util.NewLute() 607 for _, n := range nodes { 608 n.ID = ast.NewNodeID() 609 n.SetIALAttr("id", n.ID) 610 611 op := &Operation{Context: map[string]any{"ignoreProcess": "true"}} 612 op.ID = n.ID 613 op.Action = "insert" 614 op.Data = luteEngine.RenderNodeBlockDOM(n) 615 transaction.DoOperations = append(transaction.DoOperations, op) 616 617 op = &Operation{} 618 op.ID = n.ID 619 op.Action = "delete" 620 transaction.UndoOperations = append(transaction.UndoOperations, op) 621 } 622 return 623} 624 625func GetHeadingChildrenIDs(id string) (ret []string) { 626 tree, err := LoadTreeByBlockID(id) 627 if err != nil { 628 return 629 } 630 heading := treenode.GetNodeInTree(tree, id) 631 if nil == heading || ast.NodeHeading != heading.Type { 632 return 633 } 634 635 children := treenode.HeadingChildren(heading) 636 nodes := append([]*ast.Node{}, children...) 637 for _, n := range nodes { 638 ret = append(ret, n.ID) 639 } 640 return 641} 642 643func AppendHeadingChildren(id, childrenDOM string) { 644 tree, err := LoadTreeByBlockID(id) 645 if err != nil { 646 return 647 } 648 649 heading := treenode.GetNodeInTree(tree, id) 650 if nil == heading || ast.NodeHeading != heading.Type { 651 return 652 } 653 654 luteEngine := util.NewLute() 655 subTree := luteEngine.BlockDOM2Tree(childrenDOM) 656 var nodes []*ast.Node 657 for n := subTree.Root.FirstChild; nil != n; n = n.Next { 658 nodes = append(nodes, n) 659 } 660 661 slices.Reverse(nodes) 662 for _, n := range nodes { 663 heading.InsertAfter(n) 664 } 665 666 if err = indexWriteTreeUpsertQueue(tree); err != nil { 667 return 668 } 669} 670 671func GetHeadingChildrenDOM(id string, removeFoldAttr bool) (ret string) { 672 tree, err := LoadTreeByBlockID(id) 673 if err != nil { 674 return 675 } 676 heading := treenode.GetNodeInTree(tree, id) 677 if nil == heading || ast.NodeHeading != heading.Type { 678 return 679 } 680 681 nodes := append([]*ast.Node{}, heading) 682 children := treenode.HeadingChildren(heading) 683 nodes = append(nodes, children...) 684 685 for _, child := range children { 686 ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus { 687 if !entering { 688 return ast.WalkContinue 689 } 690 691 if removeFoldAttr { 692 n.RemoveIALAttr("heading-fold") 693 n.RemoveIALAttr("fold") 694 } 695 return ast.WalkContinue 696 }) 697 698 if removeFoldAttr { 699 child.RemoveIALAttr("parent-heading") 700 } else { 701 child.SetIALAttr("parent-heading", id) 702 } 703 } 704 705 if removeFoldAttr { 706 heading.RemoveIALAttr("fold") 707 heading.RemoveIALAttr("heading-fold") 708 } 709 710 luteEngine := util.NewLute() 711 ret = renderBlockDOMByNodes(nodes, luteEngine) 712 return 713} 714 715func GetHeadingLevelTransaction(id string, level int) (transaction *Transaction, err error) { 716 tree, err := LoadTreeByBlockID(id) 717 if err != nil { 718 return 719 } 720 721 node := treenode.GetNodeInTree(tree, id) 722 if nil == node { 723 err = errors.New(fmt.Sprintf(Conf.Language(15), id)) 724 return 725 } 726 727 if ast.NodeHeading != node.Type { 728 return 729 } 730 731 hLevel := node.HeadingLevel 732 if hLevel == level { 733 return 734 } 735 736 diff := level - hLevel 737 var children, childrenHeadings []*ast.Node 738 children = append(children, node) 739 children = append(children, treenode.HeadingChildren(node)...) 740 for _, c := range children { 741 ccH := c.ChildrenByType(ast.NodeHeading) 742 childrenHeadings = append(childrenHeadings, ccH...) 743 } 744 fillBlockRefCount(childrenHeadings) 745 746 transaction = &Transaction{} 747 luteEngine := util.NewLute() 748 for _, c := range childrenHeadings { 749 op := &Operation{} 750 op.ID = c.ID 751 op.Action = "update" 752 op.Data = luteEngine.RenderNodeBlockDOM(c) 753 transaction.UndoOperations = append(transaction.UndoOperations, op) 754 755 c.HeadingLevel += diff 756 if 6 < c.HeadingLevel { 757 c.HeadingLevel = 6 758 } else if 1 > c.HeadingLevel { 759 c.HeadingLevel = 1 760 } 761 762 op = &Operation{} 763 op.ID = c.ID 764 op.Action = "update" 765 op.Data = luteEngine.RenderNodeBlockDOM(c) 766 transaction.DoOperations = append(transaction.DoOperations, op) 767 } 768 return 769} 770 771func GetBlockDOM(id string) (ret string) { 772 if "" == id { 773 return 774 } 775 776 doms := GetBlockDOMs([]string{id}) 777 ret = doms[id] 778 return 779} 780 781func GetBlockDOMs(ids []string) (ret map[string]string) { 782 ret = map[string]string{} 783 if 0 == len(ids) { 784 return 785 } 786 787 luteEngine := NewLute() 788 trees := filesys.LoadTrees(ids) 789 for id, tree := range trees { 790 node := treenode.GetNodeInTree(tree, id) 791 if nil == node { 792 continue 793 } 794 795 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus { 796 if !entering || !n.IsBlock() { 797 return ast.WalkContinue 798 } 799 800 if parentFoldedHeading := treenode.GetParentFoldedHeading(n); nil != parentFoldedHeading { 801 n.SetIALAttr("parent-heading", parentFoldedHeading.ID) 802 } 803 return ast.WalkContinue 804 }) 805 806 ret[id] = luteEngine.RenderNodeBlockDOM(node) 807 } 808 return 809} 810 811func GetBlockDOMWithEmbed(id string) (ret string) { 812 if "" == id { 813 return 814 } 815 816 doms := GetBlockDOMsWithEmbed([]string{id}) 817 ret = doms[id] 818 return 819} 820 821func GetBlockDOMsWithEmbed(ids []string) (ret map[string]string) { 822 ret = map[string]string{} 823 if 0 == len(ids) { 824 return 825 } 826 827 luteEngine := NewLute() 828 trees := filesys.LoadTrees(ids) 829 for id, tree := range trees { 830 node := treenode.GetNodeInTree(tree, id) 831 if nil == node { 832 continue 833 } 834 835 resolveEmbedContent(node, luteEngine) 836 837 // 处理折叠标题 838 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus { 839 if !entering || !n.IsBlock() { 840 return ast.WalkContinue 841 } 842 843 if parentFoldedHeading := treenode.GetParentFoldedHeading(n); nil != parentFoldedHeading { 844 n.SetIALAttr("parent-heading", parentFoldedHeading.ID) 845 } 846 return ast.WalkContinue 847 }) 848 849 htmlContent := luteEngine.RenderNodeBlockDOM(node) 850 851 htmlContent = processEmbedHTML(htmlContent) 852 853 ret[id] = htmlContent 854 } 855 return 856} 857 858func resolveEmbedContent(n *ast.Node, luteEngine *lute.Lute) { 859 ast.Walk(n, func(node *ast.Node, entering bool) ast.WalkStatus { 860 if !entering || ast.NodeBlockQueryEmbed != node.Type { 861 return ast.WalkContinue 862 } 863 864 // 获取嵌入块的查询语句 865 scriptNode := node.ChildByType(ast.NodeBlockQueryEmbedScript) 866 if nil == scriptNode { 867 return ast.WalkContinue 868 } 869 stmt := scriptNode.TokensStr() 870 stmt = html.UnescapeString(stmt) 871 stmt = strings.ReplaceAll(stmt, editor.IALValEscNewLine, "\n") 872 873 // 执行查询获取嵌入的块 874 sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, Conf.Search.Limit) 875 876 // 收集所有嵌入块的内容 HTML 877 var embedContents []string 878 for _, sqlBlock := range sqlBlocks { 879 if "query_embed" == sqlBlock.Type { 880 continue 881 } 882 883 subTree, _ := LoadTreeByBlockID(sqlBlock.ID) 884 if nil == subTree { 885 continue 886 } 887 888 // 将内容转换为 HTML,直接使用原始 AST 节点渲染以保持正确的 data-node-id 889 var contentHTML string 890 if "d" == sqlBlock.Type { 891 // 文档块:直接使用原始 AST 节点渲染,保持原始的 data-node-id 892 contentHTML = luteEngine.RenderNodeBlockDOM(subTree.Root) 893 } else if "h" == sqlBlock.Type { 894 // 标题块:使用标题及其子块的原始 AST 节点渲染 895 h := treenode.GetNodeInTree(subTree, sqlBlock.ID) 896 if nil == h { 897 continue 898 } 899 var hChildren []*ast.Node 900 hChildren = append(hChildren, h) 901 hChildren = append(hChildren, treenode.HeadingChildren(h)...) 902 903 // 创建一个临时的文档节点来包含所有子节点 904 tempRoot := &ast.Node{Type: ast.NodeDocument} 905 for _, hChild := range hChildren { 906 tempRoot.AppendChild(hChild) 907 } 908 contentHTML = luteEngine.RenderNodeBlockDOM(tempRoot) 909 } else { 910 // 其他块:直接使用原始 AST 节点渲染 911 blockNode := treenode.GetNodeInTree(subTree, sqlBlock.ID) 912 if nil == blockNode { 913 continue 914 } 915 contentHTML = luteEngine.RenderNodeBlockDOM(blockNode) 916 } 917 918 if contentHTML != "" { 919 embedContents = append(embedContents, contentHTML) 920 } 921 } 922 923 // 如果有内容,在嵌入块上添加内容标记 924 if len(embedContents) > 0 { 925 node.SetIALAttr("embed-content", strings.Join(embedContents, "")) 926 } 927 928 return ast.WalkContinue 929 }) 930} 931 932func processEmbedHTML(htmlStr string) string { 933 // 使用正则表达式查找所有带有 embed-content 属性的嵌入块 934 embedPattern := `<div[^>]*data-type="NodeBlockQueryEmbed"[^>]*embed-content="[^"]*"[^>]*>` 935 re := regexp.MustCompile(embedPattern) 936 937 return re.ReplaceAllStringFunc(htmlStr, func(match string) string { 938 // 提取 embed-content 属性值 939 contentPattern := `embed-content="([^"]*)"` 940 contentRe := regexp.MustCompile(contentPattern) 941 contentMatches := contentRe.FindStringSubmatch(match) 942 943 if len(contentMatches) > 1 { 944 embedContent := contentMatches[1] 945 // HTML 解码 946 embedContent = html.UnescapeString(embedContent) 947 948 // 移除 embed-content 属性,避免在最终 HTML 中显示 949 cleanMatch := contentRe.ReplaceAllString(match, "") 950 951 // 将内容插入到嵌入块内部 952 return cleanMatch + embedContent + "</div>" 953 } 954 955 return match 956 }) 957} 958 959func GetBlockKramdown(id, mode string) (ret string) { 960 if "" == id { 961 return 962 } 963 964 tree, err := LoadTreeByBlockID(id) 965 if err != nil { 966 return 967 } 968 969 addBlockIALNodes(tree, false) 970 node := treenode.GetNodeInTree(tree, id) 971 root := &ast.Node{Type: ast.NodeDocument} 972 root.AppendChild(node.Next) // IAL 973 root.PrependChild(node) 974 luteEngine := NewLute() 975 if "md" == mode { 976 // `/api/block/getBlockKramdown` link/image URLs are no longer encoded with spaces https://github.com/siyuan-note/siyuan/issues/15611 977 luteEngine.SetPreventEncodeLinkSpace(true) 978 979 ret = treenode.ExportNodeStdMd(root, luteEngine) 980 } else { 981 tree.Root = root 982 formatRenderer := render.NewFormatRenderer(tree, luteEngine.RenderOptions) 983 ret = string(formatRenderer.Render()) 984 } 985 return 986} 987 988type ChildBlock struct { 989 ID string `json:"id"` 990 Type string `json:"type"` 991 SubType string `json:"subType,omitempty"` 992 Content string `json:"content,omitempty"` 993 Markdown string `json:"markdown,omitempty"` 994} 995 996func GetChildBlocks(id string) (ret []*ChildBlock) { 997 ret = []*ChildBlock{} 998 if "" == id { 999 return 1000 } 1001 1002 tree, err := LoadTreeByBlockID(id) 1003 if err != nil { 1004 return 1005 } 1006 1007 node := treenode.GetNodeInTree(tree, id) 1008 if nil == node { 1009 return 1010 } 1011 1012 if ast.NodeHeading == node.Type { 1013 children := treenode.HeadingChildren(node) 1014 for _, c := range children { 1015 block := sql.BuildBlockFromNode(c, tree) 1016 ret = append(ret, &ChildBlock{ 1017 ID: c.ID, 1018 Type: treenode.TypeAbbr(c.Type.String()), 1019 SubType: treenode.SubTypeAbbr(c), 1020 Content: block.Content, 1021 Markdown: block.Markdown, 1022 }) 1023 } 1024 return 1025 } 1026 1027 if !node.IsContainerBlock() { 1028 return 1029 } 1030 1031 for c := node.FirstChild; nil != c; c = c.Next { 1032 if !c.IsBlock() { 1033 continue 1034 } 1035 1036 block := sql.BuildBlockFromNode(c, tree) 1037 ret = append(ret, &ChildBlock{ 1038 ID: c.ID, 1039 Type: treenode.TypeAbbr(c.Type.String()), 1040 SubType: treenode.SubTypeAbbr(c), 1041 Content: block.Content, 1042 Markdown: block.Markdown, 1043 }) 1044 } 1045 return 1046} 1047 1048func GetTailChildBlocks(id string, n int) (ret []*ChildBlock) { 1049 ret = []*ChildBlock{} 1050 if "" == id { 1051 return 1052 } 1053 1054 tree, err := LoadTreeByBlockID(id) 1055 if err != nil { 1056 return 1057 } 1058 1059 node := treenode.GetNodeInTree(tree, id) 1060 if nil == node { 1061 return 1062 } 1063 1064 if ast.NodeHeading == node.Type { 1065 children := treenode.HeadingChildren(node) 1066 for i := len(children) - 1; 0 <= i; i-- { 1067 c := children[i] 1068 block := sql.BuildBlockFromNode(c, tree) 1069 ret = append(ret, &ChildBlock{ 1070 ID: c.ID, 1071 Type: treenode.TypeAbbr(c.Type.String()), 1072 SubType: treenode.SubTypeAbbr(c), 1073 Content: block.Content, 1074 Markdown: block.Markdown, 1075 }) 1076 if n == len(ret) { 1077 return 1078 } 1079 } 1080 return 1081 } 1082 1083 if !node.IsContainerBlock() { 1084 return 1085 } 1086 1087 for c := node.LastChild; nil != c; c = c.Previous { 1088 if !c.IsBlock() { 1089 continue 1090 } 1091 1092 block := sql.BuildBlockFromNode(c, tree) 1093 ret = append(ret, &ChildBlock{ 1094 ID: c.ID, 1095 Type: treenode.TypeAbbr(c.Type.String()), 1096 SubType: treenode.SubTypeAbbr(c), 1097 Content: block.Content, 1098 Markdown: block.Markdown, 1099 }) 1100 1101 if n == len(ret) { 1102 return 1103 } 1104 } 1105 return 1106} 1107 1108func GetBlock(id string, tree *parse.Tree) (ret *Block, err error) { 1109 ret, err = getBlock(id, tree) 1110 return 1111} 1112 1113func getBlock(id string, tree *parse.Tree) (ret *Block, err error) { 1114 if "" == id { 1115 return 1116 } 1117 1118 if nil == tree { 1119 tree, err = LoadTreeByBlockID(id) 1120 if err != nil { 1121 time.Sleep(1 * time.Second) 1122 tree, err = LoadTreeByBlockID(id) 1123 if err != nil { 1124 return 1125 } 1126 } 1127 } 1128 1129 node := treenode.GetNodeInTree(tree, id) 1130 if nil == node { 1131 err = ErrBlockNotFound 1132 return 1133 } 1134 1135 sqlBlock := sql.BuildBlockFromNode(node, tree) 1136 if nil == sqlBlock { 1137 return 1138 } 1139 ret = fromSQLBlock(sqlBlock, "", 0) 1140 return 1141} 1142 1143func getEmbeddedBlock(trees map[string]*parse.Tree, sqlBlock *sql.Block, headingMode int, breadcrumb bool) (block *Block, blockPaths []*BlockPath) { 1144 tree, _ := trees[sqlBlock.RootID] 1145 if nil == tree { 1146 tree, _ = LoadTreeByBlockID(sqlBlock.RootID) 1147 } 1148 if nil == tree { 1149 return 1150 } 1151 def := treenode.GetNodeInTree(tree, sqlBlock.ID) 1152 if nil == def { 1153 return 1154 } 1155 1156 var unlinks, nodes []*ast.Node 1157 ast.Walk(def, func(n *ast.Node, entering bool) ast.WalkStatus { 1158 if !entering { 1159 return ast.WalkContinue 1160 } 1161 1162 if ast.NodeHeading == n.Type { 1163 if "1" == n.IALAttr("fold") { 1164 children := treenode.HeadingChildren(n) 1165 for _, c := range children { 1166 unlinks = append(unlinks, c) 1167 } 1168 } 1169 } 1170 return ast.WalkContinue 1171 }) 1172 for _, n := range unlinks { 1173 n.Unlink() 1174 } 1175 // headingMode: 0=显示标题与下方的块,1=仅显示标题,2=仅显示标题下方的块 1176 if ast.NodeHeading == def.Type { 1177 if 1 == headingMode { 1178 // 仅显示标题 1179 nodes = append(nodes, def) 1180 } else if 2 == headingMode { 1181 // 仅显示标题下方的块(去除标题) 1182 if "1" != def.IALAttr("fold") { 1183 children := treenode.HeadingChildren(def) 1184 for _, c := range children { 1185 if "1" == c.IALAttr("heading-fold") { 1186 // 嵌入块包含折叠标题时不应该显示其下方块 https://github.com/siyuan-note/siyuan/issues/4765 1187 continue 1188 } 1189 nodes = append(nodes, c) 1190 } 1191 } 1192 } else { 1193 // 0: 显示标题与下方的块 1194 nodes = append(nodes, def) 1195 if "1" != def.IALAttr("fold") { 1196 children := treenode.HeadingChildren(def) 1197 for _, c := range children { 1198 if "1" == c.IALAttr("heading-fold") { 1199 // 嵌入块包含折叠标题时不应该显示其下方块 https://github.com/siyuan-note/siyuan/issues/4765 1200 continue 1201 } 1202 nodes = append(nodes, c) 1203 } 1204 } 1205 } 1206 } else { 1207 // 非标题块,直接添加 1208 nodes = append(nodes, def) 1209 } 1210 1211 b := treenode.GetBlockTree(def.ID) 1212 if nil == b { 1213 return 1214 } 1215 1216 // 嵌入块查询结果中显示块引用计数 https://github.com/siyuan-note/siyuan/issues/7191 1217 fillBlockRefCount(nodes) 1218 1219 luteEngine := NewLute() 1220 luteEngine.RenderOptions.ProtyleContenteditable = false // 不可编辑 1221 dom := renderBlockDOMByNodes(nodes, luteEngine) 1222 content := renderBlockContentByNodes(nodes) 1223 block = &Block{Box: def.Box, Path: def.Path, HPath: b.HPath, ID: def.ID, Type: def.Type.String(), Content: dom, Markdown: content /* 这里使用 Markdown 字段来临时存储 content */} 1224 1225 if "" != sqlBlock.IAL { 1226 block.IAL = map[string]string{} 1227 ialStr := strings.TrimPrefix(sqlBlock.IAL, "{:") 1228 ialStr = strings.TrimSuffix(ialStr, "}") 1229 ial := parse.Tokens2IAL([]byte(ialStr)) 1230 for _, kv := range ial { 1231 block.IAL[kv[0]] = kv[1] 1232 } 1233 } 1234 1235 if breadcrumb { 1236 blockPaths = buildBlockBreadcrumb(def, nil, true, headingMode) 1237 } 1238 if 1 > len(blockPaths) { 1239 blockPaths = []*BlockPath{} 1240 } 1241 return 1242}