path.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. // SiYuan - Build Your Eternal Digital Garden
  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. "bytes"
  19. "os"
  20. "path"
  21. "path/filepath"
  22. "sort"
  23. "strings"
  24. "github.com/88250/gulu"
  25. "github.com/88250/lute/ast"
  26. "github.com/siyuan-note/logging"
  27. "github.com/siyuan-note/siyuan/kernel/search"
  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. func createDocsByHPath(boxID, hPath, content string) (id string, err error) {
  33. hPath = strings.TrimSuffix(hPath, ".sy")
  34. if docExist := nil != treenode.GetBlockTreeRootByHPath(boxID, hPath); docExist {
  35. hPath += "-" + gulu.Rand.String(7)
  36. }
  37. pathBuilder := bytes.Buffer{}
  38. pathBuilder.WriteString("/")
  39. hPathBuilder := bytes.Buffer{}
  40. hPathBuilder.WriteString("/")
  41. parts := strings.Split(hPath, "/")[1:]
  42. for i, part := range parts {
  43. hPathBuilder.WriteString(part)
  44. hp := hPathBuilder.String()
  45. root := treenode.GetBlockTreeRootByHPath(boxID, hp)
  46. isNotLast := i < len(parts)-1
  47. if nil == root {
  48. id = ast.NewNodeID()
  49. pathBuilder.WriteString(id)
  50. docP := pathBuilder.String() + ".sy"
  51. if isNotLast {
  52. if err = createDoc(boxID, docP, part, ""); nil != err {
  53. return
  54. }
  55. } else {
  56. if err = createDoc(boxID, docP, part, content); nil != err {
  57. return
  58. }
  59. }
  60. if isNotLast {
  61. dirPath := filepath.Join(util.DataDir, boxID, pathBuilder.String())
  62. if err = os.MkdirAll(dirPath, 0755); nil != err {
  63. logging.LogErrorf("mkdir [%s] failed: %s", dirPath, err)
  64. return
  65. }
  66. }
  67. } else {
  68. id = root.ID
  69. pathBuilder.WriteString(root.ID)
  70. if !isNotLast {
  71. pathBuilder.WriteString(".sy")
  72. }
  73. }
  74. if isNotLast {
  75. pathBuilder.WriteString("/")
  76. hPathBuilder.WriteString("/")
  77. }
  78. }
  79. return
  80. }
  81. func toFlatTree(blocks []*Block, baseDepth int, typ string) (ret []*Path) {
  82. var blockRoots []*Block
  83. for _, block := range blocks {
  84. root := getBlockIn(blockRoots, block.RootID)
  85. if nil == root {
  86. root, _ = getBlock(block.RootID)
  87. blockRoots = append(blockRoots, root)
  88. }
  89. if nil == root {
  90. return
  91. }
  92. block.Depth = baseDepth + 1
  93. block.Count = len(block.Children)
  94. root.Children = append(root.Children, block)
  95. }
  96. for _, root := range blockRoots {
  97. treeNode := &Path{
  98. ID: root.ID,
  99. Box: root.Box,
  100. Name: path.Base(root.HPath),
  101. NodeType: root.Type,
  102. Type: typ,
  103. SubType: root.SubType,
  104. Depth: baseDepth,
  105. Count: len(root.Children),
  106. Updated: root.IAL["updated"],
  107. Created: root.ID[:14],
  108. }
  109. for _, c := range root.Children {
  110. treeNode.Blocks = append(treeNode.Blocks, c)
  111. }
  112. ret = append(ret, treeNode)
  113. }
  114. sort.Slice(ret, func(i, j int) bool {
  115. return ret[i].ID > ret[j].ID
  116. })
  117. return
  118. }
  119. func toSubTree(blocks []*Block, keyword string) (ret []*Path) {
  120. keyword = strings.TrimSpace(keyword)
  121. var blockRoots []*Block
  122. for _, block := range blocks {
  123. root := getBlockIn(blockRoots, block.RootID)
  124. if nil == root {
  125. root, _ = getBlock(block.RootID)
  126. blockRoots = append(blockRoots, root)
  127. }
  128. block.Depth = 1
  129. block.Count = len(block.Children)
  130. root.Children = append(root.Children, block)
  131. }
  132. for _, root := range blockRoots {
  133. treeNode := &Path{
  134. ID: root.ID,
  135. Box: root.Box,
  136. Name: path.Base(root.HPath),
  137. Type: "backlink",
  138. NodeType: "NodeDocument",
  139. SubType: root.SubType,
  140. Depth: 0,
  141. Count: len(root.Children),
  142. }
  143. for _, c := range root.Children {
  144. if "NodeListItem" == c.Type {
  145. tree, _ := loadTreeByBlockID(c.RootID)
  146. li := treenode.GetNodeInTree(tree, c.ID)
  147. if nil == li || nil == li.FirstChild {
  148. // 反链面板拖拽到文档以后可能会出现这种情况 https://github.com/siyuan-note/siyuan/issues/5363
  149. continue
  150. }
  151. var first *sql.Block
  152. if 3 != li.ListData.Typ {
  153. first = sql.GetBlock(li.FirstChild.ID)
  154. } else {
  155. first = sql.GetBlock(li.FirstChild.Next.ID)
  156. }
  157. name := first.Content
  158. parentPos := 0
  159. if "" != keyword {
  160. parentPos, name = search.MarkText(name, keyword, 12, Conf.Search.CaseSensitive)
  161. }
  162. subRoot := &Path{
  163. ID: li.ID,
  164. Box: li.Box,
  165. Name: name,
  166. Type: "backlink",
  167. NodeType: li.Type.String(),
  168. SubType: c.SubType,
  169. Depth: 1,
  170. Count: 1,
  171. }
  172. unfold := true
  173. for liFirstBlockSpan := li.FirstChild.FirstChild; nil != liFirstBlockSpan; liFirstBlockSpan = liFirstBlockSpan.Next {
  174. if treenode.IsBlockRef(liFirstBlockSpan) {
  175. continue
  176. }
  177. if "" != strings.TrimSpace(liFirstBlockSpan.Text()) {
  178. unfold = false
  179. break
  180. }
  181. }
  182. for next := li.FirstChild.Next; nil != next; next = next.Next {
  183. subBlock, _ := getBlock(next.ID)
  184. if unfold {
  185. if ast.NodeList == next.Type {
  186. for subLi := next.FirstChild; nil != subLi; subLi = subLi.Next {
  187. subLiBlock, _ := getBlock(subLi.ID)
  188. var subFirst *sql.Block
  189. if 3 != subLi.ListData.Typ {
  190. subFirst = sql.GetBlock(subLi.FirstChild.ID)
  191. } else {
  192. subFirst = sql.GetBlock(subLi.FirstChild.Next.ID)
  193. }
  194. subPos := 0
  195. content := subFirst.Content
  196. if "" != keyword {
  197. subPos, content = search.MarkText(subFirst.Content, keyword, 12, Conf.Search.CaseSensitive)
  198. }
  199. if -1 < subPos {
  200. parentPos = 0 // 需要显示父级
  201. }
  202. subLiBlock.Content = content
  203. subLiBlock.Depth = 2
  204. subRoot.Blocks = append(subRoot.Blocks, subLiBlock)
  205. }
  206. } else if ast.NodeHeading == next.Type {
  207. subBlock.Depth = 2
  208. subRoot.Blocks = append(subRoot.Blocks, subBlock)
  209. headingChildren := treenode.HeadingChildren(next)
  210. var breakSub bool
  211. for _, n := range headingChildren {
  212. block, _ := getBlock(n.ID)
  213. subPos := 0
  214. content := block.Content
  215. if "" != keyword {
  216. subPos, content = search.MarkText(block.Content, keyword, 12, Conf.Search.CaseSensitive)
  217. }
  218. if -1 < subPos {
  219. parentPos = 0
  220. }
  221. block.Content = content
  222. block.Depth = 3
  223. subRoot.Blocks = append(subRoot.Blocks, block)
  224. if ast.NodeHeading == n.Type {
  225. // 跳过子标题下面的块
  226. breakSub = true
  227. break
  228. }
  229. }
  230. if breakSub {
  231. break
  232. }
  233. } else {
  234. if nil == treenode.HeadingParent(next) {
  235. subBlock.Depth = 2
  236. subRoot.Blocks = append(subRoot.Blocks, subBlock)
  237. }
  238. }
  239. }
  240. }
  241. if -1 < parentPos {
  242. treeNode.Children = append(treeNode.Children, subRoot)
  243. }
  244. } else if "NodeHeading" == c.Type {
  245. tree, _ := loadTreeByBlockID(c.RootID)
  246. h := treenode.GetNodeInTree(tree, c.ID)
  247. if nil == h {
  248. continue
  249. }
  250. name := sql.GetBlock(h.ID).Content
  251. parentPos := 0
  252. if "" != keyword {
  253. parentPos, name = search.MarkText(name, keyword, 12, Conf.Search.CaseSensitive)
  254. }
  255. subRoot := &Path{
  256. ID: h.ID,
  257. Box: h.Box,
  258. Name: name,
  259. Type: "backlink",
  260. NodeType: h.Type.String(),
  261. SubType: c.SubType,
  262. Depth: 1,
  263. Count: 1,
  264. }
  265. unfold := true
  266. for headingFirstSpan := h.FirstChild; nil != headingFirstSpan; headingFirstSpan = headingFirstSpan.Next {
  267. if treenode.IsBlockRef(headingFirstSpan) {
  268. continue
  269. }
  270. if "" != strings.TrimSpace(headingFirstSpan.Text()) {
  271. unfold = false
  272. break
  273. }
  274. }
  275. if unfold {
  276. headingChildren := treenode.HeadingChildren(h)
  277. for _, headingChild := range headingChildren {
  278. if ast.NodeList == headingChild.Type {
  279. for subLi := headingChild.FirstChild; nil != subLi; subLi = subLi.Next {
  280. subLiBlock, _ := getBlock(subLi.ID)
  281. var subFirst *sql.Block
  282. if 3 != subLi.ListData.Typ {
  283. subFirst = sql.GetBlock(subLi.FirstChild.ID)
  284. } else {
  285. subFirst = sql.GetBlock(subLi.FirstChild.Next.ID)
  286. }
  287. subPos := 0
  288. content := subFirst.Content
  289. if "" != keyword {
  290. subPos, content = search.MarkText(content, keyword, 12, Conf.Search.CaseSensitive)
  291. }
  292. if -1 < subPos {
  293. parentPos = 0
  294. }
  295. subLiBlock.Content = subFirst.Content
  296. subLiBlock.Depth = 2
  297. subRoot.Blocks = append(subRoot.Blocks, subLiBlock)
  298. }
  299. } else {
  300. subBlock, _ := getBlock(headingChild.ID)
  301. subBlock.Depth = 2
  302. subRoot.Blocks = append(subRoot.Blocks, subBlock)
  303. }
  304. }
  305. }
  306. if -1 < parentPos {
  307. treeNode.Children = append(treeNode.Children, subRoot)
  308. }
  309. } else {
  310. pos := 0
  311. content := c.Content
  312. if "" != keyword {
  313. pos, content = search.MarkText(content, keyword, 12, Conf.Search.CaseSensitive)
  314. }
  315. if -1 < pos {
  316. treeNode.Blocks = append(treeNode.Blocks, c)
  317. }
  318. }
  319. }
  320. rootPos := -1
  321. var rootContent string
  322. if "" != keyword {
  323. rootPos, rootContent = search.MarkText(treeNode.Name, keyword, 12, Conf.Search.CaseSensitive)
  324. treeNode.Name = rootContent
  325. }
  326. if 0 < len(treeNode.Children) || 0 < len(treeNode.Blocks) || (-1 < rootPos && "" != keyword) {
  327. ret = append(ret, treeNode)
  328. }
  329. }
  330. sort.Slice(ret, func(i, j int) bool {
  331. return ret[i].ID > ret[j].ID
  332. })
  333. return
  334. }
  335. func getBlockIn(blocks []*Block, id string) *Block {
  336. if "" == id {
  337. return nil
  338. }
  339. for _, block := range blocks {
  340. if block.ID == id {
  341. return block
  342. }
  343. }
  344. return nil
  345. }