blockinfo.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  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. package model
  17. import (
  18. "os"
  19. "path/filepath"
  20. "sort"
  21. "strings"
  22. "unicode/utf8"
  23. "github.com/88250/gulu"
  24. "github.com/88250/lute/ast"
  25. "github.com/88250/lute/editor"
  26. "github.com/88250/lute/parse"
  27. "github.com/siyuan-note/logging"
  28. "github.com/siyuan-note/siyuan/kernel/sql"
  29. "github.com/siyuan-note/siyuan/kernel/treenode"
  30. "github.com/siyuan-note/siyuan/kernel/util"
  31. )
  32. type BlockInfo struct {
  33. ID string `json:"id"`
  34. RootID string `json:"rootID"`
  35. Name string `json:"name"`
  36. RefCount int `json:"refCount"`
  37. SubFileCount int `json:"subFileCount"`
  38. RefIDs []string `json:"refIDs"`
  39. IAL map[string]string `json:"ial"`
  40. Icon string `json:"icon"`
  41. }
  42. func GetDocInfo(blockID string) (ret *BlockInfo) {
  43. WaitForWritingFiles()
  44. tree, err := loadTreeByBlockID(blockID)
  45. if nil != err {
  46. logging.LogErrorf("load tree by root id [%s] failed: %s", blockID, err)
  47. return
  48. }
  49. title := tree.Root.IALAttr("title")
  50. ret = &BlockInfo{ID: blockID, RootID: tree.Root.ID, Name: title}
  51. ret.IAL = parse.IAL2Map(tree.Root.KramdownIAL)
  52. scrollData := ret.IAL["scroll"]
  53. if 0 < len(scrollData) {
  54. scroll := map[string]interface{}{}
  55. if parseErr := gulu.JSON.UnmarshalJSON([]byte(scrollData), &scroll); nil != parseErr {
  56. logging.LogWarnf("parse scroll data [%s] failed: %s", scrollData, parseErr)
  57. delete(ret.IAL, "scroll")
  58. } else {
  59. if zoomInId := scroll["zoomInId"]; nil != zoomInId {
  60. if nil == treenode.GetBlockTree(zoomInId.(string)) {
  61. delete(ret.IAL, "scroll")
  62. }
  63. } else {
  64. if startId := scroll["startId"]; nil != startId {
  65. if nil == treenode.GetBlockTree(startId.(string)) {
  66. delete(ret.IAL, "scroll")
  67. }
  68. }
  69. if endId := scroll["endId"]; nil != endId {
  70. if nil == treenode.GetBlockTree(endId.(string)) {
  71. delete(ret.IAL, "scroll")
  72. }
  73. }
  74. }
  75. }
  76. }
  77. ret.RefIDs, _ = sql.QueryRefIDsByDefID(blockID, false)
  78. ret.RefCount = len(ret.RefIDs)
  79. var subFileCount int
  80. boxLocalPath := filepath.Join(util.DataDir, tree.Box)
  81. subFiles, err := os.ReadDir(filepath.Join(boxLocalPath, strings.TrimSuffix(tree.Path, ".sy")))
  82. if nil == err {
  83. for _, subFile := range subFiles {
  84. if strings.HasSuffix(subFile.Name(), ".sy") {
  85. subFileCount++
  86. }
  87. }
  88. }
  89. ret.SubFileCount = subFileCount
  90. ret.Icon = tree.Root.IALAttr("icon")
  91. return
  92. }
  93. func GetBlockRefText(id string) string {
  94. bt := treenode.GetBlockTree(id)
  95. if nil == bt {
  96. return ErrBlockNotFound.Error()
  97. }
  98. tree, err := loadTreeByBlockID(id)
  99. if nil != err {
  100. return ErrTreeNotFound.Error()
  101. }
  102. node := treenode.GetNodeInTree(tree, id)
  103. if nil == node {
  104. return ErrBlockNotFound.Error()
  105. }
  106. return getNodeRefText(node)
  107. }
  108. func getBlockRefText(id string, tree *parse.Tree) (ret string) {
  109. node := treenode.GetNodeInTree(tree, id)
  110. if nil == node {
  111. return
  112. }
  113. ret = getNodeRefText(node)
  114. ret = maxContent(ret, Conf.Editor.BlockRefDynamicAnchorTextMaxLen)
  115. return
  116. }
  117. func getNodeRefText(node *ast.Node) string {
  118. if ret := node.IALAttr("name"); "" != ret {
  119. ret = strings.TrimSpace(ret)
  120. ret = util.EscapeHTML(ret)
  121. return ret
  122. }
  123. return getNodeRefText0(node)
  124. }
  125. func getNodeRefText0(node *ast.Node) string {
  126. switch node.Type {
  127. case ast.NodeBlockQueryEmbed:
  128. return "Query Embed Block..."
  129. case ast.NodeIFrame:
  130. return "IFrame..."
  131. case ast.NodeThematicBreak:
  132. return "Thematic Break..."
  133. case ast.NodeVideo:
  134. return "Video..."
  135. case ast.NodeAudio:
  136. return "Audio..."
  137. }
  138. if ast.NodeDocument != node.Type && node.IsContainerBlock() {
  139. node = treenode.FirstLeafBlock(node)
  140. }
  141. ret := renderBlockText(node, nil)
  142. if Conf.Editor.BlockRefDynamicAnchorTextMaxLen < utf8.RuneCountInString(ret) {
  143. ret = gulu.Str.SubStr(ret, Conf.Editor.BlockRefDynamicAnchorTextMaxLen) + "..."
  144. }
  145. return ret
  146. }
  147. func GetBlockRefIDs(id string) (refIDs, refTexts, defIDs []string) {
  148. refIDs = []string{}
  149. refTexts = []string{}
  150. defIDs = []string{}
  151. bt := treenode.GetBlockTree(id)
  152. if nil == bt {
  153. return
  154. }
  155. isDoc := bt.ID == bt.RootID
  156. refIDs, refTexts = sql.QueryRefIDsByDefID(id, isDoc)
  157. if isDoc {
  158. defIDs = sql.QueryChildDefIDsByRootDefID(id)
  159. } else {
  160. defIDs = append(defIDs, id)
  161. }
  162. return
  163. }
  164. func GetBlockRefIDsByFileAnnotationID(id string) (refIDs, refTexts []string) {
  165. refIDs, refTexts = sql.QueryRefIDsByAnnotationID(id)
  166. return
  167. }
  168. func GetBlockDefIDsByRefText(refText string, excludeIDs []string) (ret []string) {
  169. ret = sql.QueryBlockDefIDsByRefText(refText, excludeIDs)
  170. sort.Sort(sort.Reverse(sort.StringSlice(ret)))
  171. if 1 > len(ret) {
  172. ret = []string{}
  173. }
  174. return
  175. }
  176. func GetBlockIndex(id string) (ret int) {
  177. tree, _ := loadTreeByBlockID(id)
  178. if nil == tree {
  179. return
  180. }
  181. node := treenode.GetNodeInTree(tree, id)
  182. if nil == node {
  183. return
  184. }
  185. rootChild := node
  186. for ; nil == rootChild.Parent || ast.NodeDocument != rootChild.Parent.Type; rootChild = rootChild.Parent {
  187. }
  188. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  189. if !entering {
  190. return ast.WalkContinue
  191. }
  192. if !n.IsChildBlockOf(tree.Root, 1) {
  193. return ast.WalkContinue
  194. }
  195. ret++
  196. if n.ID == rootChild.ID {
  197. return ast.WalkStop
  198. }
  199. return ast.WalkContinue
  200. })
  201. return
  202. }
  203. type BlockPath struct {
  204. ID string `json:"id"`
  205. Name string `json:"name"`
  206. Type string `json:"type"`
  207. SubType string `json:"subType"`
  208. Children []*BlockPath `json:"children"`
  209. }
  210. func BuildBlockBreadcrumb(id string, excludeTypes []string) (ret []*BlockPath, err error) {
  211. ret = []*BlockPath{}
  212. tree, err := loadTreeByBlockID(id)
  213. if nil == tree {
  214. err = nil
  215. return
  216. }
  217. node := treenode.GetNodeInTree(tree, id)
  218. if nil == node {
  219. return
  220. }
  221. ret = buildBlockBreadcrumb(node, excludeTypes)
  222. return
  223. }
  224. func buildBlockBreadcrumb(node *ast.Node, excludeTypes []string) (ret []*BlockPath) {
  225. ret = []*BlockPath{}
  226. if nil == node {
  227. return
  228. }
  229. box := Conf.Box(node.Box)
  230. if nil == box {
  231. return
  232. }
  233. headingLevel := 16
  234. maxNameLen := 1024
  235. var hPath string
  236. baseBlock := treenode.GetBlockTreeRootByPath(node.Box, node.Path)
  237. if nil != baseBlock {
  238. hPath = baseBlock.HPath
  239. }
  240. for parent := node; nil != parent; parent = parent.Parent {
  241. if "" == parent.ID {
  242. continue
  243. }
  244. id := parent.ID
  245. fc := parent.FirstChild
  246. if nil != fc && ast.NodeTaskListItemMarker == fc.Type {
  247. fc = fc.Next
  248. }
  249. name := util.EscapeHTML(parent.IALAttr("name"))
  250. if ast.NodeDocument == parent.Type {
  251. name = util.EscapeHTML(box.Name) + util.EscapeHTML(hPath)
  252. } else {
  253. if "" == name {
  254. if ast.NodeListItem == parent.Type {
  255. name = gulu.Str.SubStr(renderBlockText(fc, excludeTypes), maxNameLen)
  256. } else {
  257. name = gulu.Str.SubStr(renderBlockText(parent, excludeTypes), maxNameLen)
  258. }
  259. }
  260. if ast.NodeHeading == parent.Type {
  261. headingLevel = parent.HeadingLevel
  262. }
  263. }
  264. add := true
  265. if ast.NodeList == parent.Type || ast.NodeSuperBlock == parent.Type || ast.NodeBlockquote == parent.Type {
  266. add = false
  267. }
  268. if ast.NodeParagraph == parent.Type && nil != parent.Parent && ast.NodeListItem == parent.Parent.Type && nil == parent.Next && (nil == parent.Previous || ast.NodeTaskListItemMarker == parent.Previous.Type) {
  269. add = false
  270. }
  271. if ast.NodeListItem == parent.Type {
  272. if "" == name {
  273. name = gulu.Str.SubStr(renderBlockText(fc, excludeTypes), maxNameLen)
  274. }
  275. }
  276. name = strings.ReplaceAll(name, editor.Caret, "")
  277. if add {
  278. ret = append([]*BlockPath{{
  279. ID: id,
  280. Name: util.EscapeHTML(name),
  281. Type: parent.Type.String(),
  282. SubType: treenode.SubTypeAbbr(parent),
  283. }}, ret...)
  284. }
  285. for prev := parent.Previous; nil != prev; prev = prev.Previous {
  286. b := prev
  287. if ast.NodeSuperBlock == prev.Type {
  288. // 超级块中包含标题块时下方块面包屑计算不正确 https://github.com/siyuan-note/siyuan/issues/6675
  289. b = treenode.SuperBlockLastHeading(prev)
  290. if nil == b {
  291. // 超级块下方块被作为嵌入块时设置显示面包屑后不渲染 https://github.com/siyuan-note/siyuan/issues/6690
  292. b = prev
  293. }
  294. }
  295. if ast.NodeHeading == b.Type && headingLevel > b.HeadingLevel {
  296. name = gulu.Str.SubStr(renderBlockText(b, excludeTypes), maxNameLen)
  297. ret = append([]*BlockPath{{
  298. ID: b.ID,
  299. Name: util.EscapeHTML(name),
  300. Type: b.Type.String(),
  301. SubType: treenode.SubTypeAbbr(b),
  302. }}, ret...)
  303. headingLevel = b.HeadingLevel
  304. }
  305. }
  306. }
  307. return
  308. }