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 "errors"
21 "fmt"
22 "io/fs"
23 "os"
24 "path"
25 "path/filepath"
26 "sort"
27 "strconv"
28 "strings"
29 "sync"
30 "time"
31 "unicode/utf8"
32
33 "github.com/88250/go-humanize"
34 "github.com/88250/gulu"
35 "github.com/88250/lute"
36 "github.com/88250/lute/ast"
37 "github.com/88250/lute/html"
38 "github.com/88250/lute/parse"
39 util2 "github.com/88250/lute/util"
40 "github.com/siyuan-note/filelock"
41 "github.com/siyuan-note/logging"
42 "github.com/siyuan-note/riff"
43 "github.com/siyuan-note/siyuan/kernel/av"
44 "github.com/siyuan-note/siyuan/kernel/cache"
45 "github.com/siyuan-note/siyuan/kernel/filesys"
46 "github.com/siyuan-note/siyuan/kernel/search"
47 "github.com/siyuan-note/siyuan/kernel/sql"
48 "github.com/siyuan-note/siyuan/kernel/task"
49 "github.com/siyuan-note/siyuan/kernel/treenode"
50 "github.com/siyuan-note/siyuan/kernel/util"
51)
52
53type File struct {
54 Path string `json:"path"`
55 Name string `json:"name"` // 标题,即 ial["title"]
56 Icon string `json:"icon"`
57 Name1 string `json:"name1"` // 命名,即 ial["name"]
58 Alias string `json:"alias"`
59 Memo string `json:"memo"`
60 Bookmark string `json:"bookmark"`
61 ID string `json:"id"`
62 Count int `json:"count"`
63 Size uint64 `json:"size"`
64 HSize string `json:"hSize"`
65 Mtime int64 `json:"mtime"`
66 CTime int64 `json:"ctime"`
67 HMtime string `json:"hMtime"`
68 HCtime string `json:"hCtime"`
69 Sort int `json:"sort"`
70 SubFileCount int `json:"subFileCount"`
71 Hidden bool `json:"hidden"`
72
73 NewFlashcardCount int `json:"newFlashcardCount"`
74 DueFlashcardCount int `json:"dueFlashcardCount"`
75 FlashcardCount int `json:"flashcardCount"`
76}
77
78func (box *Box) docFromFileInfo(fileInfo *FileInfo, ial map[string]string) (ret *File) {
79 ret = &File{}
80 ret.Path = fileInfo.path
81 ret.Size = uint64(fileInfo.size)
82 ret.Name = ial["title"] + ".sy"
83 ret.Icon = ial["icon"]
84 ret.ID = ial["id"]
85 ret.Name1 = ial["name"]
86 ret.Alias = ial["alias"]
87 ret.Memo = ial["memo"]
88 ret.Bookmark = ial["bookmark"]
89 t, _ := time.ParseInLocation("20060102150405", ret.ID[:14], time.Local)
90 ret.CTime = t.Unix()
91 ret.HCtime = t.Format("2006-01-02 15:04:05") + ", " + util.HumanizeTime(t, Conf.Lang)
92 ret.HSize = humanize.BytesCustomCeil(ret.Size, 2)
93
94 mTime := t
95 if updated := ial["updated"]; "" != updated {
96 if updatedTime, err := time.ParseInLocation("20060102150405", updated, time.Local); err == nil {
97 mTime = updatedTime
98 }
99 }
100
101 ret.Mtime = mTime.Unix()
102 ret.HMtime = mTime.Format("2006-01-02 15:04:05") + ", " + util.HumanizeTime(mTime, Conf.Lang)
103 return
104}
105
106func (box *Box) docIAL(p string) (ret map[string]string) {
107 name := strings.ToLower(filepath.Base(p))
108 if !strings.HasSuffix(name, ".sy") {
109 return nil
110 }
111
112 ret = cache.GetDocIAL(p)
113 if nil != ret {
114 return ret
115 }
116
117 filePath := filepath.Join(util.DataDir, box.ID, p)
118 ret = filesys.DocIAL(filePath)
119 if 1 > len(ret) {
120 logging.LogWarnf("properties not found in file [%s]", filePath)
121 box.moveCorruptedData(filePath)
122 return nil
123 }
124 cache.PutDocIAL(p, ret)
125 return ret
126}
127
128func (box *Box) moveCorruptedData(filePath string) {
129 base := filepath.Base(filePath)
130 to := filepath.Join(util.WorkspaceDir, "corrupted", time.Now().Format("2006-01-02-150405"), box.ID, base)
131 if copyErr := filelock.Copy(filePath, to); nil != copyErr {
132 logging.LogErrorf("copy corrupted data file [%s] failed: %s", filePath, copyErr)
133 return
134 }
135 if removeErr := filelock.Remove(filePath); nil != removeErr {
136 logging.LogErrorf("remove corrupted data file [%s] failed: %s", filePath, removeErr)
137 return
138 }
139 logging.LogWarnf("moved corrupted data file [%s] to [%s]", filePath, to)
140}
141
142func SearchDocsByKeyword(keyword string, flashcard bool) (ret []map[string]string) {
143 ret = []map[string]string{}
144
145 var deck *riff.Deck
146 var deckBlockIDs []string
147 if flashcard {
148 deck = Decks[builtinDeckID]
149 if nil == deck {
150 return
151 }
152
153 deckBlockIDs = deck.GetBlockIDs()
154 }
155
156 openedBoxes := Conf.GetOpenedBoxes()
157 boxes := map[string]*Box{}
158 for _, box := range openedBoxes {
159 boxes[box.ID] = box
160 }
161
162 keywords := strings.Fields(keyword)
163 var rootBlocks []*sql.Block
164 if 0 < len(keywords) {
165 for _, box := range boxes {
166 if gulu.Str.Contains(box.Name, keywords) {
167 if flashcard {
168 newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs)
169 if 0 < flashcardCount {
170 ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon, "newFlashcardCount": strconv.Itoa(newFlashcardCount), "dueFlashcardCount": strconv.Itoa(dueFlashcardCount), "flashcardCount": strconv.Itoa(flashcardCount)})
171 }
172 } else {
173 ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon})
174 }
175 }
176 }
177
178 var condition string
179 for i, k := range keywords {
180 condition += "(hpath LIKE '%" + k + "%'"
181 namCondition := Conf.Search.NAMFilter(k)
182 condition += " " + namCondition
183 condition += ")"
184
185 if i < len(keywords)-1 {
186 condition += " AND "
187 }
188 }
189
190 rootBlocks = sql.QueryRootBlockByCondition(condition, Conf.Search.Limit)
191 } else {
192 for _, box := range boxes {
193 if flashcard {
194 newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs)
195 if 0 < flashcardCount {
196 ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon, "newFlashcardCount": strconv.Itoa(newFlashcardCount), "dueFlashcardCount": strconv.Itoa(dueFlashcardCount), "flashcardCount": strconv.Itoa(flashcardCount)})
197 }
198 } else {
199 ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon})
200 }
201 }
202 }
203
204 for _, rootBlock := range rootBlocks {
205 b := boxes[rootBlock.Box]
206 if nil == b {
207 continue
208 }
209 hPath := b.Name + rootBlock.HPath
210 if flashcard {
211 newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootBlock.ID, deck, deckBlockIDs)
212 if 0 < flashcardCount {
213 ret = append(ret, map[string]string{"path": rootBlock.Path, "hPath": hPath, "box": rootBlock.Box, "boxIcon": b.Icon, "newFlashcardCount": strconv.Itoa(newFlashcardCount), "dueFlashcardCount": strconv.Itoa(dueFlashcardCount), "flashcardCount": strconv.Itoa(flashcardCount)})
214 }
215 } else {
216 ret = append(ret, map[string]string{"path": rootBlock.Path, "hPath": hPath, "box": rootBlock.Box, "boxIcon": b.Icon})
217 }
218 }
219
220 sort.Slice(ret, func(i, j int) bool {
221 return ret[i]["hPath"] < ret[j]["hPath"]
222 })
223 return
224}
225
226type FileInfo struct {
227 path string
228 name string
229 size int64
230 isdir bool
231}
232
233func ListDocTree(boxID, listPath string, sortMode int, flashcard, showHidden bool, maxListCount int) (ret []*File, totals int, err error) {
234 //os.MkdirAll("pprof", 0755)
235 //cpuProfile, _ := os.Create("pprof/cpu_profile_list_doc_tree")
236 //pprof.StartCPUProfile(cpuProfile)
237 //defer pprof.StopCPUProfile()
238
239 ret = []*File{}
240
241 var deck *riff.Deck
242 var deckBlockIDs []string
243 if flashcard {
244 deck = Decks[builtinDeckID]
245 if nil == deck {
246 return
247 }
248
249 deckBlockIDs = deck.GetBlockIDs()
250 }
251
252 box := Conf.Box(boxID)
253 if nil == box {
254 return nil, 0, errors.New(Conf.Language(0))
255 }
256
257 boxConf := box.GetConf()
258
259 if util.SortModeUnassigned == sortMode {
260 sortMode = Conf.FileTree.Sort
261 if util.SortModeFileTree != boxConf.SortMode {
262 sortMode = boxConf.SortMode
263 }
264 }
265
266 var files []*FileInfo
267 start := time.Now()
268 files, totals, err = box.Ls(listPath)
269 if err != nil {
270 return
271 }
272 elapsed := time.Now().Sub(start).Milliseconds()
273 if 100 < elapsed {
274 logging.LogWarnf("ls elapsed [%dms]", elapsed)
275 }
276
277 start = time.Now()
278 boxLocalPath := filepath.Join(util.DataDir, box.ID)
279 var docs []*File
280 for _, file := range files {
281 if file.isdir {
282 if !ast.IsNodeIDPattern(file.name) {
283 continue
284 }
285
286 parentDocPath := strings.TrimSuffix(file.path, "/") + ".sy"
287 parentDocFile := box.Stat(parentDocPath)
288 if nil == parentDocFile {
289 continue
290 }
291 if ial := box.docIAL(parentDocPath); nil != ial {
292 if !showHidden && "true" == ial["custom-hidden"] {
293 continue
294 }
295
296 doc := box.docFromFileInfo(parentDocFile, ial)
297 subFiles, err := os.ReadDir(filepath.Join(boxLocalPath, file.path))
298 if err == nil {
299 for _, subFile := range subFiles {
300 subDocFilePath := path.Join(file.path, subFile.Name())
301 if subIAL := box.docIAL(subDocFilePath); "true" == subIAL["custom-hidden"] {
302 continue
303 }
304
305 if strings.HasSuffix(subFile.Name(), ".sy") {
306 doc.SubFileCount++
307 }
308 }
309 }
310
311 if flashcard {
312 rootID := util.GetTreeID(parentDocPath)
313 newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootID, deck, deckBlockIDs)
314 if 0 < flashcardCount {
315 doc.NewFlashcardCount = newFlashcardCount
316 doc.DueFlashcardCount = dueFlashcardCount
317 doc.FlashcardCount = flashcardCount
318 docs = append(docs, doc)
319 }
320 } else {
321 docs = append(docs, doc)
322 }
323 }
324
325 continue
326 } else {
327 if strings.HasSuffix(file.name, ".sy") && !ast.IsNodeIDPattern(strings.TrimSuffix(file.name, ".sy")) {
328 // 不以块 ID 命名的 .sy 文件不应该被加载到思源中 https://github.com/siyuan-note/siyuan/issues/16089
329 continue
330 }
331 }
332
333 subFolder := filepath.Join(boxLocalPath, strings.TrimSuffix(file.path, ".sy"))
334 if gulu.File.IsDir(subFolder) {
335 continue
336 }
337
338 if ial := box.docIAL(file.path); nil != ial {
339 if !showHidden && "true" == ial["custom-hidden"] {
340 continue
341 }
342
343 doc := box.docFromFileInfo(file, ial)
344
345 if flashcard {
346 rootID := util.GetTreeID(file.path)
347 newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootID, deck, deckBlockIDs)
348 if 0 < flashcardCount {
349 doc.NewFlashcardCount = newFlashcardCount
350 doc.DueFlashcardCount = dueFlashcardCount
351 doc.FlashcardCount = flashcardCount
352 docs = append(docs, doc)
353 }
354 } else {
355 docs = append(docs, doc)
356 }
357 }
358 }
359 elapsed = time.Now().Sub(start).Milliseconds()
360 if 500 < elapsed {
361 logging.LogWarnf("list doc tree [%s] build docs [%d] elapsed [%dms]", listPath, len(docs), elapsed)
362 }
363
364 start = time.Now()
365 refCount := sql.QueryRootBlockRefCount()
366 for _, doc := range docs {
367 if count := refCount[doc.ID]; 0 < count {
368 doc.Count = count
369 }
370 }
371 elapsed = time.Now().Sub(start).Milliseconds()
372 if 500 < elapsed {
373 logging.LogWarnf("query root block ref count elapsed [%dms]", elapsed)
374 }
375
376 start = time.Now()
377 switch sortMode {
378 case util.SortModeNameASC:
379 sort.Slice(docs, func(i, j int) bool {
380 return util.PinYinCompare4FileTree(docs[i].Name, docs[j].Name)
381 })
382 case util.SortModeNameDESC:
383 sort.Slice(docs, func(i, j int) bool {
384 return util.PinYinCompare4FileTree(docs[j].Name, docs[i].Name)
385 })
386 case util.SortModeUpdatedASC:
387 sort.Slice(docs, func(i, j int) bool { return docs[i].Mtime < docs[j].Mtime })
388 case util.SortModeUpdatedDESC:
389 sort.Slice(docs, func(i, j int) bool { return docs[i].Mtime > docs[j].Mtime })
390 case util.SortModeAlphanumASC:
391 sort.Slice(docs, func(i, j int) bool {
392 return util.NaturalCompare(docs[i].Name, docs[j].Name)
393 })
394 case util.SortModeAlphanumDESC:
395 sort.Slice(docs, func(i, j int) bool {
396 return util.NaturalCompare(docs[j].Name, docs[i].Name)
397 })
398 case util.SortModeCustom:
399 fileTreeFiles := docs
400 box.fillSort(&fileTreeFiles)
401 sort.Slice(fileTreeFiles, func(i, j int) bool {
402 if fileTreeFiles[i].Sort == fileTreeFiles[j].Sort {
403 return util.TimeFromID(fileTreeFiles[i].ID) > util.TimeFromID(fileTreeFiles[j].ID)
404 }
405 return fileTreeFiles[i].Sort < fileTreeFiles[j].Sort
406 })
407 ret = append(ret, fileTreeFiles...)
408 totals = len(ret)
409 if maxListCount < len(ret) {
410 ret = ret[:maxListCount]
411 }
412 ret = ret[:]
413 return
414 case util.SortModeRefCountASC:
415 sort.Slice(docs, func(i, j int) bool { return docs[i].Count < docs[j].Count })
416 case util.SortModeRefCountDESC:
417 sort.Slice(docs, func(i, j int) bool { return docs[i].Count > docs[j].Count })
418 case util.SortModeCreatedASC:
419 sort.Slice(docs, func(i, j int) bool { return docs[i].CTime < docs[j].CTime })
420 case util.SortModeCreatedDESC:
421 sort.Slice(docs, func(i, j int) bool { return docs[i].CTime > docs[j].CTime })
422 case util.SortModeSizeASC:
423 sort.Slice(docs, func(i, j int) bool { return docs[i].Size < docs[j].Size })
424 case util.SortModeSizeDESC:
425 sort.Slice(docs, func(i, j int) bool { return docs[i].Size > docs[j].Size })
426 case util.SortModeSubDocCountASC:
427 sort.Slice(docs, func(i, j int) bool { return docs[i].SubFileCount < docs[j].SubFileCount })
428 case util.SortModeSubDocCountDESC:
429 sort.Slice(docs, func(i, j int) bool { return docs[i].SubFileCount > docs[j].SubFileCount })
430 }
431
432 if util.SortModeCustom != sortMode {
433 ret = append(ret, docs...)
434 }
435
436 totals = len(ret)
437 if maxListCount < len(ret) {
438 ret = ret[:maxListCount]
439 }
440 ret = ret[:]
441
442 elapsed = time.Now().Sub(start).Milliseconds()
443 if 200 < elapsed {
444 logging.LogInfof("sort docs elapsed [%dms]", elapsed)
445 }
446 return
447}
448
449func GetDoc(startID, endID, id string, index int, query string, queryTypes map[string]bool, queryMethod, mode int, size int, isBacklink bool, originalRefBlockIDs map[string]string, highlight bool) (
450 blockCount int, dom, parentID, parent2ID, rootID, typ string, eof, scroll bool, boxID, docPath string, isBacklinkExpand bool, keywords []string, err error) {
451 //os.MkdirAll("pprof", 0755)
452 //cpuProfile, _ := os.Create("pprof/GetDoc")
453 //pprof.StartCPUProfile(cpuProfile)
454 //defer pprof.StopCPUProfile()
455
456 FlushTxQueue() // 写入数据时阻塞,避免获取到的数据不一致
457
458 inputIndex := index
459 tree, err := LoadTreeByBlockID(id)
460 if err != nil {
461 if ErrBlockNotFound == err {
462 if 0 == mode {
463 err = ErrTreeNotFound // 初始化打开文档时如果找不到则关闭编辑器
464 }
465 }
466 return
467 }
468 if nil == tree {
469 err = ErrBlockNotFound
470 return
471 }
472
473 luteEngine := NewLute()
474 node := treenode.GetNodeInTree(tree, id)
475 if nil == node {
476 // Unable to open the doc when the block pointed by the scroll position does not exist https://github.com/siyuan-note/siyuan/issues/9030
477 node = treenode.GetNodeInTree(tree, tree.Root.ID)
478 if nil == node {
479 err = ErrBlockNotFound
480 return
481 }
482 }
483
484 located := false
485 isDoc := ast.NodeDocument == node.Type
486 isHeading := ast.NodeHeading == node.Type
487
488 boxID = node.Box
489 docPath = node.Path
490 if isDoc {
491 if 4 == mode { // 加载文档末尾
492 node = node.LastChild
493 located = true
494 // 重新计算 index
495 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
496 if !entering {
497 return ast.WalkContinue
498 }
499
500 index++
501 return ast.WalkContinue
502 })
503 } else {
504 node = node.FirstChild
505 }
506 typ = ast.NodeDocument.String()
507 idx := 0
508 if 0 < index {
509 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
510 if !entering || !n.IsChildBlockOf(tree.Root, 1) {
511 return ast.WalkContinue
512 }
513
514 idx++
515 if index == idx {
516 node = n.DocChild()
517 if "1" == node.IALAttr("heading-fold") {
518 // 加载到折叠标题下方块的话需要回溯到上方标题块
519 for h := node.Previous; nil != h; h = h.Previous {
520 if "1" == h.IALAttr("fold") {
521 node = h
522 break
523 }
524 }
525 }
526 located = true
527 return ast.WalkStop
528 }
529 return ast.WalkContinue
530 })
531 }
532 } else {
533 if 0 == index && 0 != mode {
534 // 非文档且没有指定 index 时需要计算 index
535 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
536 if !entering {
537 return ast.WalkContinue
538 }
539
540 index++
541 if id == n.ID {
542 node = n.DocChild()
543 located = true
544 return ast.WalkStop
545 }
546 return ast.WalkContinue
547 })
548 }
549 }
550
551 if 1 < index && !located {
552 count := 0
553 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
554 if !entering {
555 return ast.WalkContinue
556 }
557
558 count++
559 if index == count {
560 node = n.DocChild()
561 return ast.WalkStop
562 }
563 return ast.WalkContinue
564 })
565 }
566
567 blockCount = tree.DocBlockCount()
568 if ast.NodeDocument == node.Type {
569 parentID = node.ID
570 parent2ID = parentID
571 } else {
572 parentID = node.Parent.ID
573 parent2ID = parentID
574 tmp := node
575 if ast.NodeListItem == node.Type {
576 // 列表项聚焦返回和面包屑保持一致 https://github.com/siyuan-note/siyuan/issues/4914
577 tmp = node.Parent
578 }
579 if headingParent := treenode.HeadingParent(tmp); nil != headingParent {
580 parent2ID = headingParent.ID
581 }
582 }
583 rootID = tree.Root.ID
584 if !isDoc {
585 typ = node.Type.String()
586 }
587
588 // 判断是否需要显示动态加载滚动条 https://github.com/siyuan-note/siyuan/issues/7693
589 childCount := 0
590 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
591 if !entering {
592 return ast.WalkContinue
593 }
594
595 if 1 > childCount {
596 childCount = 1
597 } else {
598 childCount += treenode.CountBlockNodes(n)
599 }
600
601 if childCount > Conf.Editor.DynamicLoadBlocks {
602 scroll = true
603 return ast.WalkStop
604 }
605 return ast.WalkContinue
606 })
607
608 var nodes []*ast.Node
609 if isBacklink {
610 // 引用计数浮窗请求,需要按照反链逻辑组装 https://github.com/siyuan-note/siyuan/issues/6853
611 nodes, isBacklinkExpand = getBacklinkRenderNodes(node, originalRefBlockIDs)
612 } else {
613 // 如果同时存在 startID 和 endID,并且是动态加载的情况,则只加载 startID 和 endID 之间的块 [startID, endID]
614 if "" != startID && "" != endID && scroll {
615 nodes, eof = loadNodesByStartEnd(tree, startID, endID)
616 if 1 > len(nodes) {
617 // 按 mode 加载兜底
618 nodes, eof = loadNodesByMode(node, inputIndex, mode, size, isDoc, isHeading)
619 } else {
620 // 文档块没有指定 index 时需要计算 index,否则初次打开文档时 node-index 会为 0,导致首次 Ctrl+Home 无法回到顶部
621 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
622 if !entering {
623 return ast.WalkContinue
624 }
625
626 index++
627 if nodes[0].ID == n.ID {
628 return ast.WalkStop
629 }
630 return ast.WalkContinue
631 })
632 }
633 } else {
634 nodes, eof = loadNodesByMode(node, inputIndex, mode, size, isDoc, isHeading)
635 }
636 }
637
638 refCount := sql.QueryRootChildrenRefCount(rootID)
639 virtualBlockRefKeywords := getBlockVirtualRefKeywords(tree.Root)
640
641 subTree := &parse.Tree{ID: rootID, Root: &ast.Node{Type: ast.NodeDocument}, Marks: tree.Marks}
642
643 query = filterQueryInvisibleChars(query)
644 if "" != query && (0 == queryMethod || 1 == queryMethod || 3 == queryMethod) { // 只有关键字、查询语法和正则表达式搜索支持高亮
645 typeFilter := buildTypeFilter(queryTypes)
646 switch queryMethod {
647 case 0:
648 query = stringQuery(query)
649 keywords = highlightByFTS(query, typeFilter, rootID)
650 case 1:
651 keywords = highlightByFTS(query, typeFilter, rootID)
652 case 3:
653 keywords = highlightByRegexp(query, typeFilter, rootID)
654 }
655 }
656
657 existKeywords := 0 < len(keywords)
658 for _, n := range nodes {
659 var unlinks []*ast.Node
660 ast.Walk(n, func(n *ast.Node, entering bool) ast.WalkStatus {
661 if !entering {
662 return ast.WalkContinue
663 }
664
665 if "1" == n.IALAttr("heading-fold") {
666 // 折叠标题下被引用的块无法悬浮查看
667 // The referenced block under the folded heading cannot be hovered to view https://github.com/siyuan-note/siyuan/issues/9582
668 if (0 != mode && id != n.ID) || isDoc {
669 unlinks = append(unlinks, n)
670 return ast.WalkContinue
671 }
672 }
673
674 if avs := n.IALAttr(av.NodeAttrNameAvs); "" != avs {
675 // 填充属性视图角标 Display the database title on the block superscript https://github.com/siyuan-note/siyuan/issues/10545
676 avNames := getAvNames(n.IALAttr(av.NodeAttrNameAvs))
677 if "" != avNames {
678 n.SetIALAttr(av.NodeAttrViewNames, avNames)
679 }
680 }
681
682 if "" != n.ID {
683 // 填充块引计数
684 if cnt := refCount[n.ID]; 0 < cnt {
685 n.SetIALAttr("refcount", strconv.Itoa(cnt))
686 }
687 }
688
689 if highlight && existKeywords {
690 hitBlock := false
691 for p := n.Parent; nil != p; p = p.Parent {
692 if p.ID == id {
693 hitBlock = true
694 break
695 }
696 }
697 if hitBlock {
698 if ast.NodeCodeBlockCode == n.Type && !treenode.IsChartCodeBlockCode(n) {
699 // 支持代码块搜索定位 https://github.com/siyuan-note/siyuan/issues/5520
700 code := string(n.Tokens)
701 markedCode := search.EncloseHighlighting(code, keywords, search.SearchMarkLeft, search.SearchMarkRight, Conf.Search.CaseSensitive, false)
702 if code != markedCode {
703 n.Tokens = gulu.Str.ToBytes(markedCode)
704 return ast.WalkContinue
705 }
706 } else if markReplaceSpan(n, &unlinks, keywords, search.MarkDataType, luteEngine) {
707 return ast.WalkContinue
708 }
709 }
710 }
711
712 if existKeywords && id == n.ID {
713 inlines := n.ChildrenByType(ast.NodeTextMark)
714 for _, inline := range inlines {
715 if inline.IsTextMarkType("inline-memo") && util.ContainsSubStr(inline.TextMarkInlineMemoContent, keywords) {
716 // 支持行级备注搜索定位 https://github.com/siyuan-note/siyuan/issues/13465
717 keywords = append(keywords, inline.TextMarkTextContent)
718 }
719 }
720 }
721
722 if processVirtualRef(n, &unlinks, virtualBlockRefKeywords, refCount, luteEngine) {
723 return ast.WalkContinue
724 }
725 return ast.WalkContinue
726 })
727
728 for _, unlink := range unlinks {
729 unlink.Unlink()
730 }
731
732 subTree.Root.AppendChild(n)
733 }
734
735 luteEngine.RenderOptions.NodeIndexStart = index
736 dom = luteEngine.Tree2BlockDOM(subTree, luteEngine.RenderOptions)
737
738 if 1 > len(keywords) {
739 keywords = []string{}
740 }
741 for i, keyword := range keywords {
742 keyword = strings.TrimPrefix(keyword, "#")
743 keyword = strings.TrimSuffix(keyword, "#")
744 keywords[i] = keyword
745 }
746 keywords = gulu.Str.RemoveDuplicatedElem(keywords)
747
748 go setRecentDocByTree(tree)
749 return
750}
751
752func loadNodesByStartEnd(tree *parse.Tree, startID, endID string) (nodes []*ast.Node, eof bool) {
753 node := treenode.GetNodeInTree(tree, startID)
754 if nil == node {
755 return
756 }
757 nodes = append(nodes, node)
758 for n := node.Next; nil != n; n = n.Next {
759 if treenode.IsInFoldedHeading(n, nil) {
760 continue
761 }
762 nodes = append(nodes, n)
763
764 if n.ID == endID {
765 if next := n.Next; nil == next {
766 eof = true
767 } else {
768 eof = util2.IsDocIAL(n.Tokens) || util2.IsDocIAL(next.Tokens)
769 }
770 break
771 }
772
773 if len(nodes) >= Conf.Editor.DynamicLoadBlocks {
774 // 如果加载到指定数量的块则停止加载
775 break
776 }
777 }
778 return
779}
780
781func loadNodesByMode(node *ast.Node, inputIndex, mode, size int, isDoc, isHeading bool) (nodes []*ast.Node, eof bool) {
782 if 2 == mode /* 向下 */ {
783 next := node.Next
784 if ast.NodeHeading == node.Type && "1" == node.IALAttr("fold") {
785 // 标题展开时进行动态加载导致重复内容 https://github.com/siyuan-note/siyuan/issues/4671
786 // 这里要考虑折叠标题是最后一个块的情况
787 if children := treenode.HeadingChildren(node); 0 < len(children) {
788 next = children[len(children)-1].Next
789 }
790 }
791 if nil == next {
792 eof = true
793 } else {
794 eof = util2.IsDocIAL(node.Tokens) || util2.IsDocIAL(next.Tokens)
795 }
796 }
797
798 count := 0
799 switch mode {
800 case 0: // 仅加载当前 ID
801 nodes = append(nodes, node)
802 if isDoc {
803 for n := node.Next; nil != n; n = n.Next {
804 if treenode.IsInFoldedHeading(n, nil) {
805 continue
806 }
807 nodes = append(nodes, n)
808 if 1 > count {
809 count++
810 } else {
811 count += treenode.CountBlockNodes(n)
812 }
813 if size < count {
814 break
815 }
816 }
817 } else if isHeading {
818 level := node.HeadingLevel
819 for n := node.Next; nil != n; n = n.Next {
820 if treenode.IsInFoldedHeading(n, node) {
821 // 大纲点击折叠标题跳转聚焦 https://github.com/siyuan-note/siyuan/issues/4920
822 // 多级标题折叠后上级块引浮窗中未折叠 https://github.com/siyuan-note/siyuan/issues/4997
823 continue
824 }
825 if ast.NodeHeading == n.Type {
826 if n.HeadingLevel <= level {
827 break
828 }
829 }
830 nodes = append(nodes, n)
831 count++
832 if size < count {
833 break
834 }
835 }
836 }
837 case 4: // Ctrl+End 跳转到末尾后向上加载
838 for n := node; nil != n; n = n.Previous {
839 if treenode.IsInFoldedHeading(n, nil) {
840 continue
841 }
842 nodes = append([]*ast.Node{n}, nodes...)
843 if 1 > count {
844 count++
845 } else {
846 count += treenode.CountBlockNodes(n)
847 }
848 if size < count {
849 break
850 }
851 }
852 eof = true
853 case 1: // 向上加载
854 for n := node.Previous; /* 从上一个节点开始加载 */ nil != n; n = n.Previous {
855 if treenode.IsInFoldedHeading(n, nil) {
856 continue
857 }
858 nodes = append([]*ast.Node{n}, nodes...)
859 if 1 > count {
860 count++
861 } else {
862 count += treenode.CountBlockNodes(n)
863 }
864 if size < count {
865 break
866 }
867 }
868 eof = nil == node.Previous
869 case 2: // 向下加载
870 for n := node.Next; /* 从下一个节点开始加载 */ nil != n; n = n.Next {
871 if treenode.IsInFoldedHeading(n, node) {
872 continue
873 }
874 nodes = append(nodes, n)
875 if 1 > count {
876 count++
877 } else {
878 count += treenode.CountBlockNodes(n)
879 }
880 if size < count {
881 break
882 }
883 }
884 case 3: // 上下都加载
885 for n := node; nil != n; n = n.Previous {
886 if treenode.IsInFoldedHeading(n, nil) {
887 continue
888 }
889 nodes = append([]*ast.Node{n}, nodes...)
890 if 1 > count {
891 count++
892 } else {
893 count += treenode.CountBlockNodes(n)
894 }
895 if 0 < inputIndex {
896 if 1 < count {
897 break // 滑块指示器加载
898 }
899 } else {
900 if size < count {
901 break
902 }
903 }
904 }
905 if size/2 < count {
906 size = size / 2
907 } else {
908 size = size - count
909 }
910 count = 0
911 for n := node.Next; nil != n; n = n.Next {
912 if treenode.IsInFoldedHeading(n, nil) {
913 continue
914 }
915 nodes = append(nodes, n)
916 if 1 > count {
917 count++
918 } else {
919 count += treenode.CountBlockNodes(n)
920 }
921 if 0 < inputIndex {
922 if size < count {
923 break
924 }
925 } else {
926 if size < count {
927 break
928 }
929 }
930 }
931 }
932 return
933}
934
935func writeTreeUpsertQueue(tree *parse.Tree) (err error) {
936 size, err := filesys.WriteTree(tree)
937 if err != nil {
938 return
939 }
940 sql.UpsertTreeQueue(tree)
941 refreshDocInfoWithSize(tree, size)
942 return
943}
944
945func indexWriteTreeIndexQueue(tree *parse.Tree) (err error) {
946 treenode.IndexBlockTree(tree)
947 _, err = filesys.WriteTree(tree)
948 if err != nil {
949 return
950 }
951 sql.IndexTreeQueue(tree)
952 return
953}
954
955func indexWriteTreeUpsertQueue(tree *parse.Tree) (err error) {
956 treenode.UpsertBlockTree(tree)
957 return writeTreeUpsertQueue(tree)
958}
959
960func renameWriteJSONQueue(tree *parse.Tree) (err error) {
961 size, err := filesys.WriteTree(tree)
962 if err != nil {
963 return
964 }
965 sql.RenameTreeQueue(tree)
966 treenode.UpsertBlockTree(tree)
967 refreshDocInfoWithSize(tree, size)
968 return
969}
970
971func DuplicateDoc(tree *parse.Tree) {
972 msgId := util.PushMsg(Conf.Language(116), 30000)
973 defer util.PushClearMsg(msgId)
974
975 previousPath := tree.Path
976 resetTree(tree, "Duplicated", false)
977 createTreeTx(tree)
978 box := Conf.Box(tree.Box)
979 if nil != box {
980 box.addSort(previousPath, tree.ID)
981 }
982 FlushTxQueue()
983
984 // 复制为副本时将该副本块插入到数据库中 https://github.com/siyuan-note/siyuan/issues/11959
985 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
986 if !entering || !n.IsBlock() {
987 return ast.WalkContinue
988 }
989
990 avs := n.IALAttr(av.NodeAttrNameAvs)
991 for _, avID := range strings.Split(avs, ",") {
992 if !ast.IsNodeIDPattern(avID) {
993 continue
994 }
995
996 AddAttributeViewBlock(nil, []map[string]interface{}{{
997 "id": n.ID,
998 "isDetached": false,
999 }}, avID, "", "", "", "", false, map[string]interface{}{})
1000 ReloadAttrView(avID)
1001 }
1002 return ast.WalkContinue
1003 })
1004 return
1005}
1006
1007func createTreeTx(tree *parse.Tree) {
1008 transaction := &Transaction{DoOperations: []*Operation{{Action: "create", Data: tree}}}
1009 PerformTransactions(&[]*Transaction{transaction})
1010}
1011
1012var createDocLock = sync.Mutex{}
1013
1014func CreateDocByMd(boxID, p, title, md string, sorts []string) (tree *parse.Tree, err error) {
1015 createDocLock.Lock()
1016 defer createDocLock.Unlock()
1017
1018 box := Conf.Box(boxID)
1019 if nil == box {
1020 err = errors.New(Conf.Language(0))
1021 return
1022 }
1023
1024 luteEngine := util.NewLute()
1025 dom := luteEngine.Md2BlockDOM(md, false)
1026 tree, err = createDoc(box.ID, p, title, dom)
1027 if err != nil {
1028 return
1029 }
1030
1031 FlushTxQueue()
1032 if 0 < len(sorts) {
1033 ChangeFileTreeSort(box.ID, sorts)
1034 } else {
1035 box.addMinSort(path.Dir(tree.Path), tree.ID)
1036 }
1037 return
1038}
1039
1040func CreateWithMarkdown(tags, boxID, hPath, md, parentID, id string, withMath bool, clippingHref string) (retID string, err error) {
1041 createDocLock.Lock()
1042 defer createDocLock.Unlock()
1043
1044 box := Conf.Box(boxID)
1045 if nil == box {
1046 err = errors.New(Conf.Language(0))
1047 return
1048 }
1049
1050 FlushTxQueue()
1051 luteEngine := util.NewLute()
1052 if withMath {
1053 luteEngine.SetInlineMath(true)
1054 }
1055 luteEngine.SetHTMLTag2TextMark(true)
1056 if strings.HasPrefix(clippingHref, "https://ld246.com/article/") || strings.HasPrefix(clippingHref, "https://liuyun.io/article/") {
1057 // 改进链滴剪藏 https://github.com/siyuan-note/siyuan/issues/13117
1058 enableLuteInlineSyntax(luteEngine)
1059 }
1060 dom := luteEngine.Md2BlockDOM(md, false)
1061 retID, err = createDocsByHPath(box.ID, hPath, dom, parentID, id)
1062
1063 nameValues := map[string]string{}
1064 tags = strings.TrimSpace(tags)
1065 tags = strings.ReplaceAll(tags, ",", ",")
1066 tagArray := strings.Split(tags, ",")
1067 var tmp []string
1068 for _, tag := range tagArray {
1069 tmp = append(tmp, strings.TrimSpace(tag))
1070 }
1071 tags = strings.Join(tmp, ",")
1072 nameValues["tags"] = tags
1073 SetBlockAttrs(retID, nameValues)
1074
1075 FlushTxQueue()
1076 box.addMinSort(path.Dir(hPath), retID)
1077 return
1078}
1079
1080func CreateDailyNote(boxID string) (p string, existed bool, err error) {
1081 createDocLock.Lock()
1082 defer createDocLock.Unlock()
1083
1084 box := Conf.Box(boxID)
1085 if nil == box {
1086 err = ErrBoxNotFound
1087 return
1088 }
1089
1090 boxConf := box.GetConf()
1091 if "" == boxConf.DailyNoteSavePath || "/" == boxConf.DailyNoteSavePath {
1092 err = errors.New(Conf.Language(49))
1093 return
1094 }
1095
1096 hPath, err := RenderGoTemplate(boxConf.DailyNoteSavePath)
1097 if err != nil {
1098 return
1099 }
1100
1101 FlushTxQueue()
1102
1103 hPath = util.TrimSpaceInPath(hPath)
1104 existRoot := treenode.GetBlockTreeRootByHPath(box.ID, hPath)
1105 if nil != existRoot {
1106 existed = true
1107 p = existRoot.Path
1108
1109 tree, loadErr := LoadTreeByBlockID(existRoot.RootID)
1110 if nil != loadErr {
1111 logging.LogWarnf("load tree by block id [%s] failed: %v", existRoot.RootID, loadErr)
1112 return
1113 }
1114 p = tree.Path
1115 date := time.Now().Format("20060102")
1116 if tree.Root.IALAttr("custom-dailynote-"+date) == "" {
1117 tree.Root.SetIALAttr("custom-dailynote-"+date, date)
1118 if err = indexWriteTreeUpsertQueue(tree); err != nil {
1119 return
1120 }
1121 }
1122 return
1123 }
1124
1125 id, err := createDocsByHPath(box.ID, hPath, "", "", "")
1126 if err != nil {
1127 return
1128 }
1129
1130 var templateTree *parse.Tree
1131 var templateDom string
1132 if "" != boxConf.DailyNoteTemplatePath {
1133 tplPath := filepath.Join(util.DataDir, "templates", boxConf.DailyNoteTemplatePath)
1134 if !filelock.IsExist(tplPath) {
1135 logging.LogWarnf("not found daily note template [%s]", tplPath)
1136 } else {
1137 var renderErr error
1138 templateTree, templateDom, renderErr = RenderTemplate(tplPath, id, false)
1139 if nil != renderErr {
1140 logging.LogWarnf("render daily note template [%s] failed: %s", boxConf.DailyNoteTemplatePath, err)
1141 }
1142 }
1143 }
1144 if "" != templateDom {
1145 var tree *parse.Tree
1146 tree, err = LoadTreeByBlockID(id)
1147 if err == nil {
1148 tree.Root.FirstChild.Unlink()
1149
1150 luteEngine := util.NewLute()
1151 newTree := luteEngine.BlockDOM2Tree(templateDom)
1152 var children []*ast.Node
1153 for c := newTree.Root.FirstChild; nil != c; c = c.Next {
1154 children = append(children, c)
1155 }
1156 for _, c := range children {
1157 tree.Root.AppendChild(c)
1158 }
1159
1160 // Creating a dailynote template supports doc attributes https://github.com/siyuan-note/siyuan/issues/10698
1161 templateIALs := parse.IAL2Map(templateTree.Root.KramdownIAL)
1162 for k, v := range templateIALs {
1163 if "name" == k || "alias" == k || "bookmark" == k || "memo" == k || "icon" == k || strings.HasPrefix(k, "custom-") {
1164 tree.Root.SetIALAttr(k, v)
1165 }
1166 }
1167
1168 tree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
1169 if err = indexWriteTreeUpsertQueue(tree); err != nil {
1170 return
1171 }
1172 }
1173 }
1174 IncSync()
1175
1176 FlushTxQueue()
1177
1178 tree, err := LoadTreeByBlockID(id)
1179 if err != nil {
1180 logging.LogErrorf("load tree by block id [%s] failed: %v", id, err)
1181 return
1182 }
1183 p = tree.Path
1184 date := time.Now().Format("20060102")
1185 tree.Root.SetIALAttr("custom-dailynote-"+date, date)
1186 if err = indexWriteTreeUpsertQueue(tree); err != nil {
1187 return
1188 }
1189
1190 return
1191}
1192
1193func GetHPathByPath(boxID, p string) (hPath string, err error) {
1194 if "/" == p {
1195 hPath = "/"
1196 return
1197 }
1198
1199 luteEngine := util.NewLute()
1200 tree, err := filesys.LoadTree(boxID, p, luteEngine)
1201 if err != nil {
1202 return
1203 }
1204 hPath = tree.HPath
1205 return
1206}
1207
1208func GetHPathsByPaths(paths []string) (hPaths []string, err error) {
1209 pathsBoxes := getBoxesByPaths(paths)
1210 for p, box := range pathsBoxes {
1211 if nil == box {
1212 logging.LogWarnf("box not found by path [%s]", p)
1213 continue
1214 }
1215
1216 bt := treenode.GetBlockTreeByPath(p)
1217 if nil == bt {
1218 logging.LogWarnf("block tree not found by path [%s]", p)
1219 continue
1220 }
1221
1222 hpath := html.UnescapeString(bt.HPath)
1223 hPaths = append(hPaths, box.Name+hpath)
1224 }
1225 return
1226}
1227
1228func GetHPathByID(id string) (hPath string, err error) {
1229 tree, err := LoadTreeByBlockID(id)
1230 if err != nil {
1231 return
1232 }
1233 hPath = tree.HPath
1234 return
1235}
1236
1237func GetPathByID(id string) (path, boxID string, err error) {
1238 tree, err := LoadTreeByBlockID(id)
1239 if err != nil {
1240 return
1241 }
1242
1243 path = tree.Path
1244 boxID = tree.Box
1245 return
1246}
1247
1248func GetFullHPathByID(id string) (hPath string, err error) {
1249 tree, err := LoadTreeByBlockID(id)
1250 if err != nil {
1251 return
1252 }
1253
1254 box := Conf.Box(tree.Box)
1255 if nil == box {
1256 err = ErrBoxNotFound
1257 return
1258 }
1259 hPath = box.Name + tree.HPath
1260 return
1261}
1262
1263func GetIDsByHPath(hpath, boxID string) (ret []string, err error) {
1264 ret = []string{}
1265 roots := treenode.GetBlockTreeRootsByHPath(boxID, hpath)
1266 if 1 > len(roots) {
1267 return
1268 }
1269
1270 for _, root := range roots {
1271 ret = append(ret, root.ID)
1272 }
1273 ret = gulu.Str.RemoveDuplicatedElem(ret)
1274 if 1 > len(ret) {
1275 ret = []string{}
1276 }
1277 return
1278}
1279
1280func MoveDocs(fromPaths []string, toBoxID, toPath string, callback interface{}) (err error) {
1281 toBox := Conf.Box(toBoxID)
1282 if nil == toBox {
1283 err = errors.New(Conf.Language(0))
1284 return
1285 }
1286
1287 fromPaths = util.FilterMoveDocFromPaths(fromPaths, toPath)
1288 if 1 > len(fromPaths) {
1289 return
1290 }
1291
1292 pathsBoxes := getBoxesByPaths(fromPaths)
1293
1294 if 1 == len(fromPaths) {
1295 // 移动到自己的父文档下的情况相当于不移动,直接返回
1296 if fromBox := pathsBoxes[fromPaths[0]]; nil != fromBox && fromBox.ID == toBoxID {
1297 parentDir := path.Dir(fromPaths[0])
1298 if ("/" == toPath && "/" == parentDir) || (parentDir+".sy" == toPath) {
1299 return
1300 }
1301 }
1302 }
1303
1304 // 检查路径深度是否超过限制
1305 for fromPath, fromBox := range pathsBoxes {
1306 childDepth := util.GetChildDocDepth(filepath.Join(util.DataDir, fromBox.ID, fromPath))
1307 if depth := strings.Count(toPath, "/") + childDepth; 6 < depth && !Conf.FileTree.AllowCreateDeeper {
1308 err = errors.New(Conf.Language(118))
1309 return
1310 }
1311 }
1312
1313 // A progress layer appears when moving more than 64 documents at once https://github.com/siyuan-note/siyuan/issues/9356
1314 subDocsCount := 0
1315 for fromPath, fromBox := range pathsBoxes {
1316 subDocsCount += countSubDocs(fromBox.ID, fromPath)
1317 }
1318 needShowProgress := 64 < subDocsCount
1319 if needShowProgress {
1320 defer util.PushClearProgress()
1321 }
1322
1323 FlushTxQueue()
1324 luteEngine := util.NewLute()
1325 count := 0
1326 for fromPath, fromBox := range pathsBoxes {
1327 count++
1328 if needShowProgress {
1329 util.PushEndlessProgress(fmt.Sprintf(Conf.Language(70), fmt.Sprintf("%d/%d", count, len(fromPaths))))
1330 }
1331
1332 _, err = moveDoc(fromBox, fromPath, toBox, toPath, luteEngine, callback)
1333 if err != nil {
1334 return
1335 }
1336 }
1337 cache.ClearDocsIAL()
1338 IncSync()
1339 return
1340}
1341
1342func countSubDocs(box, p string) (ret int) {
1343 p = strings.TrimSuffix(p, ".sy")
1344 _ = filelock.Walk(filepath.Join(util.DataDir, box, p), func(path string, d fs.DirEntry, err error) error {
1345 if err != nil {
1346 return err
1347 }
1348 if d.IsDir() {
1349 return nil
1350 }
1351 if strings.HasSuffix(path, ".sy") {
1352 ret++
1353 }
1354 return nil
1355 })
1356 return
1357}
1358
1359func moveDoc(fromBox *Box, fromPath string, toBox *Box, toPath string, luteEngine *lute.Lute, callback interface{}) (newPath string, err error) {
1360 isSameBox := fromBox.ID == toBox.ID
1361
1362 if isSameBox {
1363 if !fromBox.Exist(toPath) {
1364 err = ErrBlockNotFound
1365 return
1366 }
1367 } else {
1368 if !toBox.Exist(toPath) {
1369 err = ErrBlockNotFound
1370 return
1371 }
1372 }
1373
1374 tree, err := filesys.LoadTree(fromBox.ID, fromPath, luteEngine)
1375 if err != nil {
1376 err = ErrBlockNotFound
1377 return
1378 }
1379
1380 fromParentTree := loadParentTree(tree)
1381
1382 moveToRoot := "/" == toPath
1383 toBlockID := tree.ID
1384 fromFolder := path.Join(path.Dir(fromPath), tree.ID)
1385 toFolder := "/"
1386 if !moveToRoot {
1387 var toTree *parse.Tree
1388 if isSameBox {
1389 toTree, err = filesys.LoadTree(fromBox.ID, toPath, luteEngine)
1390 } else {
1391 toTree, err = filesys.LoadTree(toBox.ID, toPath, luteEngine)
1392 }
1393 if err != nil {
1394 err = ErrBlockNotFound
1395 return
1396 }
1397
1398 toBlockID = toTree.ID
1399 toFolder = path.Join(path.Dir(toPath), toBlockID)
1400 }
1401
1402 if isSameBox {
1403 if err = fromBox.MkdirAll(toFolder); err != nil {
1404 return
1405 }
1406 } else {
1407 if err = toBox.MkdirAll(toFolder); err != nil {
1408 return
1409 }
1410 }
1411
1412 needMoveSubDocs := fromBox.Exist(fromFolder)
1413 if needMoveSubDocs {
1414 // 移动子文档文件夹
1415
1416 newFolder := path.Join(toFolder, tree.ID)
1417 if isSameBox {
1418 if err = fromBox.Move(fromFolder, newFolder); err != nil {
1419 return
1420 }
1421 } else {
1422 absFromPath := filepath.Join(util.DataDir, fromBox.ID, fromFolder)
1423 absToPath := filepath.Join(util.DataDir, toBox.ID, newFolder)
1424 if filelock.IsExist(absToPath) {
1425 filelock.Remove(absToPath)
1426 }
1427 if err = filelock.Rename(absFromPath, absToPath); err != nil {
1428 msg := fmt.Sprintf(Conf.Language(5), fromBox.Name, fromPath, err)
1429 logging.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, fromBox.ID, err)
1430 err = errors.New(msg)
1431 return
1432 }
1433 }
1434 }
1435
1436 newPath = path.Join(toFolder, tree.ID+".sy")
1437
1438 if isSameBox {
1439 if err = fromBox.Move(fromPath, newPath); err != nil {
1440 return
1441 }
1442
1443 tree, err = filesys.LoadTree(fromBox.ID, newPath, luteEngine)
1444 if err != nil {
1445 return
1446 }
1447
1448 moveTree(tree)
1449 } else {
1450 absFromPath := filepath.Join(util.DataDir, fromBox.ID, fromPath)
1451 absToPath := filepath.Join(util.DataDir, toBox.ID, newPath)
1452 if err = filelock.Rename(absFromPath, absToPath); err != nil {
1453 msg := fmt.Sprintf(Conf.Language(5), fromBox.Name, fromPath, err)
1454 logging.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, fromBox.ID, err)
1455 err = errors.New(msg)
1456 return
1457 }
1458
1459 tree, err = filesys.LoadTree(toBox.ID, newPath, luteEngine)
1460 if err != nil {
1461 return
1462 }
1463
1464 moveTree(tree)
1465 moveSorts(tree.ID, fromBox.ID, toBox.ID)
1466 }
1467
1468 if needMoveSubDocs {
1469 // 将其所有子文档的移动事件推送到前端 https://github.com/siyuan-note/siyuan/issues/11661
1470 subDocsFolder := path.Join(toFolder, tree.ID)
1471 syFiles := listSyFiles(path.Join(toBox.ID, subDocsFolder))
1472 for _, syFile := range syFiles {
1473 relPath := strings.TrimPrefix(syFile, "/"+path.Join(toBox.ID, toFolder))
1474 subFromPath := path.Join(path.Dir(fromPath), relPath)
1475 subToPath := path.Join(toFolder, relPath)
1476
1477 evt := util.NewCmdResult("moveDoc", 0, util.PushModeBroadcast)
1478 evt.Data = map[string]interface{}{
1479 "fromNotebook": fromBox.ID,
1480 "fromPath": subFromPath,
1481 "toNotebook": toBox.ID,
1482 "toPath": path.Dir(subToPath) + ".sy",
1483 "newPath": subToPath,
1484 }
1485 evt.Callback = callback
1486 util.PushEvent(evt)
1487 }
1488 }
1489
1490 evt := util.NewCmdResult("moveDoc", 0, util.PushModeBroadcast)
1491 evt.Data = map[string]interface{}{
1492 "fromNotebook": fromBox.ID,
1493 "fromPath": fromPath,
1494 "toNotebook": toBox.ID,
1495 "toPath": toPath,
1496 "newPath": newPath,
1497 }
1498 evt.Callback = callback
1499 util.PushEvent(evt)
1500
1501 refreshDocInfo(fromParentTree)
1502 return
1503}
1504
1505func RemoveDoc(boxID, p string) {
1506 box := Conf.Box(boxID)
1507 if nil == box {
1508 return
1509 }
1510
1511 FlushTxQueue()
1512 luteEngine := util.NewLute()
1513 removeDoc(box, p, luteEngine)
1514 IncSync()
1515 return
1516}
1517
1518func RemoveDocs(paths []string) {
1519 util.PushEndlessProgress(Conf.Language(116))
1520 defer util.PushClearProgress()
1521
1522 paths = util.FilterSelfChildDocs(paths)
1523 pathsBoxes := getBoxesByPaths(paths)
1524 FlushTxQueue()
1525 luteEngine := util.NewLute()
1526 for p, box := range pathsBoxes {
1527 removeDoc(box, p, luteEngine)
1528 }
1529 return
1530}
1531
1532func removeDoc(box *Box, p string, luteEngine *lute.Lute) {
1533 tree, _ := filesys.LoadTree(box.ID, p, luteEngine)
1534 if nil == tree {
1535 return
1536 }
1537
1538 historyDir, err := GetHistoryDir(HistoryOpDelete)
1539 if err != nil {
1540 logging.LogErrorf("get history dir failed: %s", err)
1541 return
1542 }
1543
1544 historyPath := filepath.Join(historyDir, box.ID, p)
1545 absPath := filepath.Join(util.DataDir, box.ID, p)
1546 if err = filelock.Copy(absPath, historyPath); err != nil {
1547 logging.LogErrorf("backup [path=%s] to history [%s] failed: %s", absPath, historyPath, err)
1548 return
1549 }
1550
1551 generateAvHistory(tree, historyDir)
1552 copyDocAssetsToDataAssets(box.ID, p)
1553
1554 removeIDs := treenode.RootChildIDs(tree.ID)
1555 dir := path.Dir(p)
1556 childrenDir := path.Join(dir, tree.ID)
1557 existChildren := box.Exist(childrenDir)
1558 if existChildren {
1559 absChildrenDir := filepath.Join(util.DataDir, tree.Box, childrenDir)
1560 historyPath = filepath.Join(historyDir, tree.Box, childrenDir)
1561 if err = filelock.Copy(absChildrenDir, historyPath); err != nil {
1562 logging.LogErrorf("backup [path=%s] to history [%s] failed: %s", absChildrenDir, historyPath, err)
1563 return
1564 }
1565 }
1566 indexHistoryDir(filepath.Base(historyDir), util.NewLute())
1567
1568 allRemoveRootIDs := []string{tree.ID}
1569 allRemoveRootIDs = append(allRemoveRootIDs, removeIDs...)
1570 allRemoveRootIDs = gulu.Str.RemoveDuplicatedElem(allRemoveRootIDs)
1571 for _, rootID := range allRemoveRootIDs {
1572 removeTree, _ := LoadTreeByBlockID(rootID)
1573 if nil == removeTree {
1574 continue
1575 }
1576
1577 syncDelete2AvBlock(removeTree.Root, removeTree, nil)
1578 }
1579
1580 if existChildren {
1581 if err = box.Remove(childrenDir); err != nil {
1582 logging.LogErrorf("remove children dir [%s%s] failed: %s", box.ID, childrenDir, err)
1583 return
1584 }
1585 logging.LogInfof("removed children dir [%s%s]", box.ID, childrenDir)
1586 }
1587 if err = box.Remove(p); err != nil {
1588 logging.LogErrorf("remove [%s%s] failed: %s", box.ID, p, err)
1589 return
1590 }
1591 logging.LogInfof("removed doc [%s%s]", box.ID, p)
1592
1593 box.removeSort(removeIDs)
1594 RemoveRecentDoc(removeIDs)
1595 if "/" != dir {
1596 others, err := os.ReadDir(filepath.Join(util.DataDir, box.ID, dir))
1597 if err == nil && 1 > len(others) {
1598 box.Remove(dir)
1599 }
1600 }
1601
1602 evt := util.NewCmdResult("removeDoc", 0, util.PushModeBroadcast)
1603 evt.Data = map[string]interface{}{
1604 "ids": removeIDs,
1605 }
1606 util.PushEvent(evt)
1607
1608 refreshParentDocInfo(tree)
1609 task.AppendTask(task.DatabaseIndex, removeDoc0, tree, childrenDir)
1610}
1611
1612func removeDoc0(tree *parse.Tree, childrenDir string) {
1613 // 收集引用的定义块 ID
1614 refDefIDs := getRefDefIDs(tree.Root)
1615 // 推送定义节点引用计数
1616 for _, defID := range refDefIDs {
1617 task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, defID)
1618 }
1619
1620 treenode.RemoveBlockTreesByPathPrefix(childrenDir)
1621 sql.RemoveTreePathQueue(tree.Box, childrenDir)
1622 cache.RemoveDocIAL(tree.Path)
1623 return
1624}
1625
1626func RenameDoc(boxID, p, title string) (err error) {
1627 box := Conf.Box(boxID)
1628 if nil == box {
1629 err = errors.New(Conf.Language(0))
1630 return
1631 }
1632
1633 FlushTxQueue()
1634 luteEngine := util.NewLute()
1635 tree, err := filesys.LoadTree(box.ID, p, luteEngine)
1636 if err != nil {
1637 return
1638 }
1639
1640 title = removeInvisibleCharsInTitle(title)
1641 if 512 < utf8.RuneCountInString(title) {
1642 // 限制笔记本名和文档名最大长度为 `512` https://github.com/siyuan-note/siyuan/issues/6299
1643 return errors.New(Conf.Language(106))
1644 }
1645
1646 oldTitle := tree.Root.IALAttr("title")
1647 if oldTitle == title {
1648 return
1649 }
1650 if "" == title {
1651 title = Conf.language(16)
1652 }
1653 title = strings.ReplaceAll(title, "/", "")
1654
1655 tree.HPath = path.Join(path.Dir(tree.HPath), title)
1656 tree.Root.SetIALAttr("title", title)
1657 tree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
1658 if err = renameWriteJSONQueue(tree); err != nil {
1659 return
1660 }
1661
1662 refText := getNodeRefText(tree.Root)
1663 evt := util.NewCmdResult("rename", 0, util.PushModeBroadcast)
1664 evt.Data = map[string]interface{}{
1665 "box": boxID,
1666 "id": tree.Root.ID,
1667 "path": p,
1668 "title": title,
1669 "refText": refText,
1670 }
1671 util.PushEvent(evt)
1672
1673 box.renameSubTrees(tree)
1674 updateRefTextRenameDoc(tree)
1675 IncSync()
1676 return
1677}
1678
1679func createDoc(boxID, p, title, dom string) (tree *parse.Tree, err error) {
1680 title = removeInvisibleCharsInTitle(title)
1681 if 512 < utf8.RuneCountInString(title) {
1682 // 限制笔记本名和文档名最大长度为 `512` https://github.com/siyuan-note/siyuan/issues/6299
1683 err = errors.New(Conf.Language(106))
1684 return
1685 }
1686 title = strings.ReplaceAll(title, "/", "")
1687 title = strings.TrimSpace(title)
1688 if "" == title {
1689 title = Conf.Language(16)
1690 }
1691
1692 baseName := strings.TrimSpace(path.Base(p))
1693 if "" == util.GetTreeID(baseName) {
1694 err = errors.New(Conf.Language(16))
1695 return
1696 }
1697
1698 if strings.HasPrefix(baseName, ".") {
1699 err = errors.New(Conf.Language(13))
1700 return
1701 }
1702
1703 box := Conf.Box(boxID)
1704 if nil == box {
1705 err = errors.New(Conf.Language(0))
1706 return
1707 }
1708
1709 id := util.GetTreeID(p)
1710 var hPath string
1711 folder := path.Dir(p)
1712 if "/" != folder {
1713 parentID := path.Base(folder)
1714 parentTree, loadErr := LoadTreeByBlockID(parentID)
1715 if nil != loadErr {
1716 logging.LogErrorf("get parent tree [%s] failed", parentID)
1717 err = ErrBlockNotFound
1718 return
1719 }
1720 hPath = path.Join(parentTree.HPath, title)
1721 } else {
1722 hPath = "/" + title
1723 }
1724
1725 if depth := strings.Count(p, "/"); 7 < depth && !Conf.FileTree.AllowCreateDeeper {
1726 err = errors.New(Conf.Language(118))
1727 return
1728 }
1729
1730 if !box.Exist(folder) {
1731 if err = box.MkdirAll(folder); err != nil {
1732 return
1733 }
1734 }
1735
1736 if box.Exist(p) {
1737 err = errors.New(Conf.Language(1))
1738 return
1739 }
1740
1741 luteEngine := util.NewLute()
1742 tree = luteEngine.BlockDOM2Tree(dom)
1743 tree.Box = boxID
1744 tree.Path = p
1745 tree.HPath = hPath
1746 tree.ID = id
1747 tree.Root.ID = id
1748 tree.Root.Spec = "1"
1749 updated := util.TimeFromID(id)
1750 tree.Root.KramdownIAL = [][]string{{"id", id}, {"title", html.EscapeAttrVal(title)}, {"updated", updated}}
1751 if nil == tree.Root.FirstChild {
1752 tree.Root.AppendChild(treenode.NewParagraph(""))
1753 }
1754
1755 // 如果段落块中仅包含一个 mp3/mp4 超链接,则将其转换为音视频块
1756 // Convert mp3 and mp4 hyperlinks to audio and video when moving cloud inbox to docs https://github.com/siyuan-note/siyuan/issues/9778
1757 var unlinks []*ast.Node
1758 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
1759 if !entering {
1760 return ast.WalkContinue
1761 }
1762
1763 if ast.NodeParagraph == n.Type {
1764 link := n.FirstChild
1765 if nil != link && link.IsTextMarkType("a") {
1766 if strings.HasSuffix(link.TextMarkAHref, ".mp3") {
1767 unlinks = append(unlinks, n)
1768 audio := &ast.Node{ID: n.ID, Type: ast.NodeAudio, Tokens: []byte("<audio controls=\"controls\" src=\"" + link.TextMarkAHref + "\" data-src=\"" + link.TextMarkAHref + "\"></audio>")}
1769 audio.SetIALAttr("id", n.ID)
1770 audio.SetIALAttr("updated", util.TimeFromID(n.ID))
1771 n.InsertBefore(audio)
1772 } else if strings.HasSuffix(link.TextMarkAHref, ".mp4") {
1773 unlinks = append(unlinks, n)
1774 video := &ast.Node{ID: n.ID, Type: ast.NodeVideo, Tokens: []byte("<video controls=\"controls\" src=\"" + link.TextMarkAHref + "\" data-src=\"" + link.TextMarkAHref + "\"></video>")}
1775 video.SetIALAttr("id", n.ID)
1776 video.SetIALAttr("updated", util.TimeFromID(n.ID))
1777 n.InsertBefore(video)
1778 }
1779 }
1780 }
1781 return ast.WalkContinue
1782 })
1783 for _, unlink := range unlinks {
1784 unlink.Unlink()
1785 }
1786
1787 transaction := &Transaction{DoOperations: []*Operation{{Action: "create", Data: tree}}}
1788 PerformTransactions(&[]*Transaction{transaction})
1789 FlushTxQueue()
1790 return
1791}
1792
1793func removeInvisibleCharsInTitle(title string) string {
1794 // 不要踢掉 零宽连字符,否则有的 Emoji 会变形 https://github.com/siyuan-note/siyuan/issues/11480
1795 title = strings.ReplaceAll(title, string(gulu.ZWJ), "__@ZWJ@__")
1796 title = util.RemoveInvalid(title)
1797 title = strings.ReplaceAll(title, "__@ZWJ@__", string(gulu.ZWJ))
1798 title = strings.TrimSpace(title)
1799 return title
1800}
1801
1802func moveSorts(rootID, fromBox, toBox string) {
1803 root := treenode.GetBlockTree(rootID)
1804 if nil == root {
1805 return
1806 }
1807
1808 fromRootSorts := map[string]int{}
1809 ids := treenode.RootChildIDs(rootID)
1810 fromConfPath := filepath.Join(util.DataDir, fromBox, ".siyuan", "sort.json")
1811 fromFullSortIDs := map[string]int{}
1812 if filelock.IsExist(fromConfPath) {
1813 data, err := filelock.ReadFile(fromConfPath)
1814 if err != nil {
1815 logging.LogErrorf("read sort conf failed: %s", err)
1816 return
1817 }
1818
1819 if err = gulu.JSON.UnmarshalJSON(data, &fromFullSortIDs); err != nil {
1820 logging.LogErrorf("unmarshal sort conf failed: %s", err)
1821 }
1822 }
1823 for _, id := range ids {
1824 fromRootSorts[id] = fromFullSortIDs[id]
1825 }
1826
1827 toConfPath := filepath.Join(util.DataDir, toBox, ".siyuan", "sort.json")
1828 toFullSortIDs := map[string]int{}
1829 if filelock.IsExist(toConfPath) {
1830 data, err := filelock.ReadFile(toConfPath)
1831 if err != nil {
1832 logging.LogErrorf("read sort conf failed: %s", err)
1833 return
1834 }
1835
1836 if err = gulu.JSON.UnmarshalJSON(data, &toFullSortIDs); err != nil {
1837 logging.LogErrorf("unmarshal sort conf failed: %s", err)
1838 return
1839 }
1840 }
1841
1842 for id, sortVal := range fromRootSorts {
1843 toFullSortIDs[id] = sortVal
1844 }
1845
1846 data, err := gulu.JSON.MarshalJSON(toFullSortIDs)
1847 if err != nil {
1848 logging.LogErrorf("marshal sort conf failed: %s", err)
1849 return
1850 }
1851 if err = filelock.WriteFile(toConfPath, data); err != nil {
1852 logging.LogErrorf("write sort conf failed: %s", err)
1853 return
1854 }
1855}
1856
1857func ChangeFileTreeSort(boxID string, paths []string) {
1858 if 1 > len(paths) {
1859 return
1860 }
1861
1862 FlushTxQueue()
1863 box := Conf.Box(boxID)
1864 sortIDs := map[string]int{}
1865 max := 0
1866 for i, p := range paths {
1867 id := util.GetTreeID(p)
1868 sortIDs[id] = i + 1
1869 if i == len(paths)-1 {
1870 max = i + 2
1871 }
1872 }
1873
1874 p := paths[0]
1875 parentPath := path.Dir(p)
1876 absParentPath := filepath.Join(util.DataDir, boxID, parentPath)
1877 files, err := os.ReadDir(absParentPath)
1878 if err != nil {
1879 logging.LogErrorf("read dir [%s] failed: %s", absParentPath, err)
1880 }
1881
1882 sortFolderIDs := map[string]int{}
1883 for _, f := range files {
1884 if !strings.HasSuffix(f.Name(), ".sy") {
1885 continue
1886 }
1887
1888 id := strings.TrimSuffix(f.Name(), ".sy")
1889 val := sortIDs[id]
1890 if 0 == val {
1891 val = max
1892 max++
1893 }
1894 sortFolderIDs[id] = val
1895 }
1896
1897 confDir := filepath.Join(util.DataDir, box.ID, ".siyuan")
1898 if err = os.MkdirAll(confDir, 0755); err != nil {
1899 logging.LogErrorf("create conf dir failed: %s", err)
1900 return
1901 }
1902 confPath := filepath.Join(confDir, "sort.json")
1903 fullSortIDs := map[string]int{}
1904 var data []byte
1905 if filelock.IsExist(confPath) {
1906 data, err = filelock.ReadFile(confPath)
1907 if err != nil {
1908 logging.LogErrorf("read sort conf failed: %s", err)
1909 return
1910 }
1911
1912 if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); err != nil {
1913 logging.LogErrorf("unmarshal sort conf failed: %s", err)
1914 }
1915 }
1916
1917 for sortID, sortVal := range sortFolderIDs {
1918 fullSortIDs[sortID] = sortVal
1919 }
1920
1921 data, err = gulu.JSON.MarshalJSON(fullSortIDs)
1922 if err != nil {
1923 logging.LogErrorf("marshal sort conf failed: %s", err)
1924 return
1925 }
1926 if err = filelock.WriteFile(confPath, data); err != nil {
1927 logging.LogErrorf("write sort conf failed: %s", err)
1928 return
1929 }
1930
1931 IncSync()
1932}
1933
1934func (box *Box) fillSort(files *[]*File) {
1935 confPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json")
1936 if !filelock.IsExist(confPath) {
1937 return
1938 }
1939
1940 data, err := filelock.ReadFile(confPath)
1941 if err != nil {
1942 logging.LogErrorf("read sort conf failed: %s", err)
1943 return
1944 }
1945
1946 fullSortIDs := map[string]int{}
1947 if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); err != nil {
1948 logging.LogErrorf("unmarshal sort conf failed: %s", err)
1949 return
1950 }
1951
1952 for _, f := range *files {
1953 id := strings.TrimSuffix(f.ID, ".sy")
1954 f.Sort = fullSortIDs[id]
1955 }
1956}
1957
1958func (box *Box) removeSort(ids []string) {
1959 confPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json")
1960 if !filelock.IsExist(confPath) {
1961 return
1962 }
1963
1964 data, err := filelock.ReadFile(confPath)
1965 if err != nil {
1966 logging.LogErrorf("read sort conf failed: %s", err)
1967 return
1968 }
1969
1970 fullSortIDs := map[string]int{}
1971 if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); err != nil {
1972 logging.LogErrorf("unmarshal sort conf failed: %s", err)
1973 return
1974 }
1975
1976 for _, toRemove := range ids {
1977 delete(fullSortIDs, toRemove)
1978 }
1979
1980 data, err = gulu.JSON.MarshalJSON(fullSortIDs)
1981 if err != nil {
1982 logging.LogErrorf("marshal sort conf failed: %s", err)
1983 return
1984 }
1985 if err = filelock.WriteFile(confPath, data); err != nil {
1986 logging.LogErrorf("write sort conf failed: %s", err)
1987 return
1988 }
1989}
1990
1991func (box *Box) addMinSort(parentPath, id string) {
1992 docs, _, err := ListDocTree(box.ID, parentPath, util.SortModeUnassigned, false, false, 1)
1993 if err != nil {
1994 logging.LogErrorf("list doc tree failed: %s", err)
1995 return
1996 }
1997
1998 sortVal := 0
1999 if 0 < len(docs) {
2000 sortVal = docs[0].Sort - 1
2001 }
2002
2003 confDir := filepath.Join(util.DataDir, box.ID, ".siyuan")
2004 if err = os.MkdirAll(confDir, 0755); err != nil {
2005 logging.LogErrorf("create conf dir failed: %s", err)
2006 return
2007 }
2008 confPath := filepath.Join(confDir, "sort.json")
2009 fullSortIDs := map[string]int{}
2010 var data []byte
2011 if filelock.IsExist(confPath) {
2012 data, err = filelock.ReadFile(confPath)
2013 if err != nil {
2014 logging.LogErrorf("read sort conf failed: %s", err)
2015 return
2016 }
2017
2018 if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); err != nil {
2019 logging.LogErrorf("unmarshal sort conf failed: %s", err)
2020 }
2021 }
2022
2023 fullSortIDs[id] = sortVal
2024
2025 data, err = gulu.JSON.MarshalJSON(fullSortIDs)
2026 if err != nil {
2027 logging.LogErrorf("marshal sort conf failed: %s", err)
2028 return
2029 }
2030 if err = filelock.WriteFile(confPath, data); err != nil {
2031 logging.LogErrorf("write sort conf failed: %s", err)
2032 return
2033 }
2034}
2035
2036func (box *Box) addSort(previousPath, id string) {
2037 confDir := filepath.Join(util.DataDir, box.ID, ".siyuan")
2038 if err := os.MkdirAll(confDir, 0755); err != nil {
2039 logging.LogErrorf("create conf dir failed: %s", err)
2040 return
2041 }
2042 confPath := filepath.Join(confDir, "sort.json")
2043 fullSortIDs := map[string]int{}
2044 var data []byte
2045 if filelock.IsExist(confPath) {
2046 data, err := filelock.ReadFile(confPath)
2047 if err != nil {
2048 logging.LogErrorf("read sort conf failed: %s", err)
2049 return
2050 }
2051
2052 if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); err != nil {
2053 logging.LogErrorf("unmarshal sort conf failed: %s", err)
2054 }
2055 }
2056
2057 parentPath := path.Dir(previousPath)
2058 docs, _, err := ListDocTree(box.ID, parentPath, util.SortModeUnassigned, false, false, Conf.FileTree.MaxListCount)
2059 if err != nil {
2060 logging.LogErrorf("list doc tree failed: %s", err)
2061 return
2062 }
2063
2064 previousID := util.GetTreeID(previousPath)
2065 sortVal := 0
2066 for _, doc := range docs {
2067 fullSortIDs[doc.ID] = sortVal
2068 if doc.ID == previousID {
2069 sortVal++
2070 fullSortIDs[id] = sortVal
2071 }
2072 sortVal++
2073 }
2074
2075 data, err = gulu.JSON.MarshalJSON(fullSortIDs)
2076 if err != nil {
2077 logging.LogErrorf("marshal sort conf failed: %s", err)
2078 return
2079 }
2080 if err = filelock.WriteFile(confPath, data); err != nil {
2081 logging.LogErrorf("write sort conf failed: %s", err)
2082 return
2083 }
2084}
2085
2086func (box *Box) setSort(sortIDVals map[string]int) {
2087 confPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json")
2088 if !filelock.IsExist(confPath) {
2089 return
2090 }
2091
2092 data, err := filelock.ReadFile(confPath)
2093 if err != nil {
2094 logging.LogErrorf("read sort conf failed: %s", err)
2095 return
2096 }
2097
2098 fullSortIDs := map[string]int{}
2099 if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); err != nil {
2100 logging.LogErrorf("unmarshal sort conf failed: %s", err)
2101 return
2102 }
2103
2104 for sortID := range sortIDVals {
2105 fullSortIDs[sortID] = sortIDVals[sortID]
2106 }
2107
2108 data, err = gulu.JSON.MarshalJSON(fullSortIDs)
2109 if err != nil {
2110 logging.LogErrorf("marshal sort conf failed: %s", err)
2111 return
2112 }
2113 if err = filelock.WriteFile(confPath, data); err != nil {
2114 logging.LogErrorf("write sort conf failed: %s", err)
2115 return
2116 }
2117}