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 "os"
22 "path"
23 "path/filepath"
24 "sort"
25 "strings"
26
27 "github.com/88250/lute/ast"
28 "github.com/88250/lute/parse"
29 "github.com/siyuan-note/logging"
30 "github.com/siyuan-note/siyuan/kernel/search"
31 "github.com/siyuan-note/siyuan/kernel/sql"
32 "github.com/siyuan-note/siyuan/kernel/treenode"
33 "github.com/siyuan-note/siyuan/kernel/util"
34)
35
36func createDocsByHPath(boxID, hPath, content, parentID, id string) (retID string, err error) {
37 if "" == id {
38 id = ast.NewNodeID()
39 }
40 retID = id
41
42 hPath = strings.TrimSuffix(hPath, ".sy")
43 hPath = util.TrimSpaceInPath(hPath)
44 if "" != parentID {
45 // The save path is incorrect when creating a sub-doc by ref in a doc with the same name https://github.com/siyuan-note/siyuan/issues/8138
46 // 在指定了父文档 ID 的情况下优先查找父文档
47 parentHPath, name := path.Split(hPath)
48 parentHPath = strings.TrimSuffix(parentHPath, "/")
49 preferredParent := treenode.GetBlockTreeByHPathPreferredParentID(boxID, parentHPath, parentID)
50 if nil != preferredParent && preferredParent.RootID == parentID {
51 // 如果父文档存在且 ID 一致,则直接在父文档下创建
52 p := strings.TrimSuffix(preferredParent.Path, ".sy") + "/" + id + ".sy"
53 if _, err = createDoc(boxID, p, name, content); err != nil {
54 logging.LogErrorf("create doc [%s] failed: %s", p, err)
55 }
56 return
57 }
58 }
59
60 root := treenode.GetBlockTreeRootByPath(boxID, hPath)
61 if nil != root {
62 retID = root.ID
63 return
64 }
65
66 hPathBuilder := bytes.Buffer{}
67 hpathBtMap := map[string]*treenode.BlockTree{}
68 parts := strings.Split(hPath, "/")[1:]
69 // The subdoc creation path is unstable when a parent doc with the same name exists https://github.com/siyuan-note/siyuan/issues/9322
70 // 存在同名父文档时子文档创建路径不稳定,这里需要按照完整的 hpath 映射,不能在下面的循环中边构建 hpath 边构建 path,否则虽然 hpath 相同,但是会导致 path 组装错位
71 for i, part := range parts {
72 if i == len(parts)-1 {
73 break
74 }
75
76 hPathBuilder.WriteString("/")
77 hPathBuilder.WriteString(part)
78 hp := hPathBuilder.String()
79 root = treenode.GetBlockTreeRootByHPath(boxID, hp)
80 if nil == root {
81 break
82 }
83
84 hpathBtMap[hp] = root
85 }
86
87 pathBuilder := bytes.Buffer{}
88 pathBuilder.WriteString("/")
89 hPathBuilder = bytes.Buffer{}
90 hPathBuilder.WriteString("/")
91 for i, part := range parts {
92 hPathBuilder.WriteString(part)
93 hp := hPathBuilder.String()
94 root = hpathBtMap[hp]
95 isNotLast := i < len(parts)-1
96 if nil == root {
97 rootID := ast.NewNodeID()
98 if i == len(parts)-1 {
99 rootID = retID
100 }
101
102 pathBuilder.WriteString(rootID)
103 docP := pathBuilder.String() + ".sy"
104 if isNotLast {
105 if _, err = createDoc(boxID, docP, part, ""); err != nil {
106 return
107 }
108 } else {
109 if _, err = createDoc(boxID, docP, part, content); err != nil {
110 return
111 }
112 }
113
114 if isNotLast {
115 dirPath := filepath.Join(util.DataDir, boxID, pathBuilder.String())
116 if err = os.MkdirAll(dirPath, 0755); err != nil {
117 logging.LogErrorf("mkdir [%s] failed: %s", dirPath, err)
118 return
119 }
120 }
121 } else {
122 pathBuilder.WriteString(root.ID)
123 if !isNotLast {
124 pathBuilder.WriteString(".sy")
125 }
126 }
127
128 if isNotLast {
129 pathBuilder.WriteString("/")
130 hPathBuilder.WriteString("/")
131 }
132 }
133 return
134}
135
136func toFlatTree(blocks []*Block, baseDepth int, typ string, tree *parse.Tree) (ret []*Path) {
137 var blockRoots []*Block
138 for _, block := range blocks {
139 root := getBlockIn(blockRoots, block.RootID)
140 if nil == root {
141 root, _ = getBlock(block.RootID, tree)
142 blockRoots = append(blockRoots, root)
143 }
144 if nil == root {
145 return
146 }
147 block.Depth = baseDepth + 1
148 block.Count = len(block.Children)
149 root.Children = append(root.Children, block)
150 }
151
152 folded := false
153 if "outline" == typ {
154 folded = true
155 }
156
157 for _, root := range blockRoots {
158 treeNode := &Path{
159 ID: root.ID,
160 Box: root.Box,
161 Name: path.Base(root.HPath),
162 NodeType: root.Type,
163 Type: typ,
164 SubType: root.SubType,
165 Depth: baseDepth,
166 Count: len(root.Children),
167 Folded: folded,
168
169 Updated: root.IAL["updated"],
170 Created: root.ID[:14],
171 }
172 for _, c := range root.Children {
173 treeNode.Blocks = append(treeNode.Blocks, c)
174 }
175 ret = append(ret, treeNode)
176
177 if "backlink" == typ {
178 treeNode.HPath = root.HPath
179 }
180 }
181
182 sort.Slice(ret, func(i, j int) bool {
183 return ret[i].ID > ret[j].ID
184 })
185 return
186}
187
188func toSubTree(blocks []*Block, keyword string) (ret []*Path) {
189 keyword = strings.TrimSpace(keyword)
190 var blockRoots []*Block
191 for _, block := range blocks {
192 root := getBlockIn(blockRoots, block.RootID)
193 if nil == root {
194 root, _ = getBlock(block.RootID, nil)
195 blockRoots = append(blockRoots, root)
196 }
197 block.Depth = 1
198 block.Count = len(block.Children)
199 root.Children = append(root.Children, block)
200 }
201
202 for _, root := range blockRoots {
203 treeNode := &Path{
204 ID: root.ID,
205 Box: root.Box,
206 Name: path.Base(root.HPath),
207 Type: "backlink",
208 NodeType: "NodeDocument",
209 SubType: root.SubType,
210 Depth: 0,
211 Count: len(root.Children),
212 }
213 for _, c := range root.Children {
214 if "NodeListItem" == c.Type {
215 tree, _ := LoadTreeByBlockID(c.RootID)
216 li := treenode.GetNodeInTree(tree, c.ID)
217 if nil == li || nil == li.FirstChild {
218 // 反链面板拖拽到文档以后可能会出现这种情况 https://github.com/siyuan-note/siyuan/issues/5363
219 continue
220 }
221
222 var first *sql.Block
223 if 3 != li.ListData.Typ {
224 first = sql.GetBlock(li.FirstChild.ID)
225 } else {
226 first = sql.GetBlock(li.FirstChild.Next.ID)
227 }
228 name := first.Content
229 parentPos := 0
230 if "" != keyword {
231 parentPos, name = search.MarkText(name, keyword, 12, Conf.Search.CaseSensitive)
232 }
233 subRoot := &Path{
234 ID: li.ID,
235 Box: li.Box,
236 Name: name,
237 Type: "backlink",
238 NodeType: li.Type.String(),
239 SubType: c.SubType,
240 Depth: 1,
241 Count: 1,
242 }
243
244 unfold := true
245 for liFirstBlockSpan := li.FirstChild.FirstChild; nil != liFirstBlockSpan; liFirstBlockSpan = liFirstBlockSpan.Next {
246 if treenode.IsBlockRef(liFirstBlockSpan) {
247 continue
248 }
249 if "" != strings.TrimSpace(liFirstBlockSpan.Text()) {
250 unfold = false
251 break
252 }
253 }
254 for next := li.FirstChild.Next; nil != next; next = next.Next {
255 subBlock, _ := getBlock(next.ID, tree)
256 if unfold {
257 if ast.NodeList == next.Type {
258 for subLi := next.FirstChild; nil != subLi; subLi = subLi.Next {
259 subLiBlock, _ := getBlock(subLi.ID, tree)
260 var subFirst *sql.Block
261 if 3 != subLi.ListData.Typ {
262 subFirst = sql.GetBlock(subLi.FirstChild.ID)
263 } else {
264 subFirst = sql.GetBlock(subLi.FirstChild.Next.ID)
265 }
266 subPos := 0
267 content := subFirst.Content
268 if "" != keyword {
269 subPos, content = search.MarkText(subFirst.Content, keyword, 12, Conf.Search.CaseSensitive)
270 }
271 if -1 < subPos {
272 parentPos = 0 // 需要显示父级
273 }
274 subLiBlock.Content = content
275 subLiBlock.Depth = 2
276 subRoot.Blocks = append(subRoot.Blocks, subLiBlock)
277 }
278 } else if ast.NodeHeading == next.Type {
279 subBlock.Depth = 2
280 subRoot.Blocks = append(subRoot.Blocks, subBlock)
281 headingChildren := treenode.HeadingChildren(next)
282 var breakSub bool
283 for _, n := range headingChildren {
284 block, _ := getBlock(n.ID, tree)
285 subPos := 0
286 content := block.Content
287 if "" != keyword {
288 subPos, content = search.MarkText(block.Content, keyword, 12, Conf.Search.CaseSensitive)
289 }
290 if -1 < subPos {
291 parentPos = 0
292 }
293 block.Content = content
294 block.Depth = 3
295 subRoot.Blocks = append(subRoot.Blocks, block)
296 if ast.NodeHeading == n.Type {
297 // 跳过子标题下面的块
298 breakSub = true
299 break
300 }
301 }
302 if breakSub {
303 break
304 }
305 } else {
306 if nil == treenode.HeadingParent(next) {
307 subBlock.Depth = 2
308 subRoot.Blocks = append(subRoot.Blocks, subBlock)
309 }
310 }
311 }
312 }
313 if -1 < parentPos {
314 treeNode.Children = append(treeNode.Children, subRoot)
315 }
316 } else if "NodeHeading" == c.Type {
317 tree, _ := LoadTreeByBlockID(c.RootID)
318 h := treenode.GetNodeInTree(tree, c.ID)
319 if nil == h {
320 continue
321 }
322
323 name := sql.GetBlock(h.ID).Content
324 parentPos := 0
325 if "" != keyword {
326 parentPos, name = search.MarkText(name, keyword, 12, Conf.Search.CaseSensitive)
327 }
328 subRoot := &Path{
329 ID: h.ID,
330 Box: h.Box,
331 Name: name,
332 Type: "backlink",
333 NodeType: h.Type.String(),
334 SubType: c.SubType,
335 Depth: 1,
336 Count: 1,
337 }
338
339 unfold := true
340 for headingFirstSpan := h.FirstChild; nil != headingFirstSpan; headingFirstSpan = headingFirstSpan.Next {
341 if treenode.IsBlockRef(headingFirstSpan) {
342 continue
343 }
344 if "" != strings.TrimSpace(headingFirstSpan.Text()) {
345 unfold = false
346 break
347 }
348 }
349
350 if unfold {
351 headingChildren := treenode.HeadingChildren(h)
352 for _, headingChild := range headingChildren {
353 if ast.NodeList == headingChild.Type {
354 for subLi := headingChild.FirstChild; nil != subLi; subLi = subLi.Next {
355 subLiBlock, _ := getBlock(subLi.ID, tree)
356 var subFirst *sql.Block
357 if 3 != subLi.ListData.Typ {
358 subFirst = sql.GetBlock(subLi.FirstChild.ID)
359 } else {
360 subFirst = sql.GetBlock(subLi.FirstChild.Next.ID)
361 }
362 subPos := 0
363 content := subFirst.Content
364 if "" != keyword {
365 subPos, content = search.MarkText(content, keyword, 12, Conf.Search.CaseSensitive)
366 }
367 if -1 < subPos {
368 parentPos = 0
369 }
370 subLiBlock.Content = subFirst.Content
371 subLiBlock.Depth = 2
372 subRoot.Blocks = append(subRoot.Blocks, subLiBlock)
373 }
374 } else {
375 subBlock, _ := getBlock(headingChild.ID, tree)
376 subBlock.Depth = 2
377 subRoot.Blocks = append(subRoot.Blocks, subBlock)
378 }
379 }
380 }
381
382 if -1 < parentPos {
383 treeNode.Children = append(treeNode.Children, subRoot)
384 }
385 } else {
386 pos := 0
387 content := c.Content
388 if "" != keyword {
389 pos, content = search.MarkText(content, keyword, 12, Conf.Search.CaseSensitive)
390 }
391 if -1 < pos {
392 treeNode.Blocks = append(treeNode.Blocks, c)
393 }
394 }
395 }
396
397 rootPos := -1
398 var rootContent string
399 if "" != keyword {
400 rootPos, rootContent = search.MarkText(treeNode.Name, keyword, 12, Conf.Search.CaseSensitive)
401 treeNode.Name = rootContent
402 }
403 if 0 < len(treeNode.Children) || 0 < len(treeNode.Blocks) || (-1 < rootPos && "" != keyword) {
404 ret = append(ret, treeNode)
405 }
406 }
407
408 sort.Slice(ret, func(i, j int) bool {
409 return ret[i].ID > ret[j].ID
410 })
411 return
412}
413
414func getBlockIn(blocks []*Block, id string) *Block {
415 if "" == id {
416 return nil
417 }
418 for _, block := range blocks {
419 if block.ID == id {
420 return block
421 }
422 }
423 return nil
424}