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 "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}