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