// SiYuan - Refactor your thinking // Copyright (c) 2020-present, b3log.org // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package model import ( "bytes" "errors" "fmt" "os" "path" "path/filepath" "sort" "strconv" "strings" "sync" "time" "unicode/utf8" "github.com/88250/go-humanize" "github.com/88250/gulu" "github.com/88250/lute" "github.com/88250/lute/ast" "github.com/88250/lute/html" "github.com/88250/lute/parse" util2 "github.com/88250/lute/util" "github.com/facette/natsort" "github.com/siyuan-note/filelock" "github.com/siyuan-note/logging" "github.com/siyuan-note/riff" "github.com/siyuan-note/siyuan/kernel/av" "github.com/siyuan-note/siyuan/kernel/cache" "github.com/siyuan-note/siyuan/kernel/filesys" "github.com/siyuan-note/siyuan/kernel/search" "github.com/siyuan-note/siyuan/kernel/sql" "github.com/siyuan-note/siyuan/kernel/task" "github.com/siyuan-note/siyuan/kernel/treenode" "github.com/siyuan-note/siyuan/kernel/util" ) type File struct { Path string `json:"path"` Name string `json:"name"` // 标题,即 ial["title"] Icon string `json:"icon"` Name1 string `json:"name1"` // 命名,即 ial["name"] Alias string `json:"alias"` Memo string `json:"memo"` Bookmark string `json:"bookmark"` ID string `json:"id"` Count int `json:"count"` Size uint64 `json:"size"` HSize string `json:"hSize"` Mtime int64 `json:"mtime"` CTime int64 `json:"ctime"` HMtime string `json:"hMtime"` HCtime string `json:"hCtime"` Sort int `json:"sort"` SubFileCount int `json:"subFileCount"` Hidden bool `json:"hidden"` NewFlashcardCount int `json:"newFlashcardCount"` DueFlashcardCount int `json:"dueFlashcardCount"` FlashcardCount int `json:"flashcardCount"` } func (box *Box) docFromFileInfo(fileInfo *FileInfo, ial map[string]string) (ret *File) { ret = &File{} ret.Path = fileInfo.path ret.Size = uint64(fileInfo.size) ret.Name = ial["title"] + ".sy" ret.Icon = ial["icon"] ret.ID = ial["id"] ret.Name1 = ial["name"] ret.Alias = ial["alias"] ret.Memo = ial["memo"] ret.Bookmark = ial["bookmark"] t, _ := time.ParseInLocation("20060102150405", ret.ID[:14], time.Local) ret.CTime = t.Unix() ret.HCtime = t.Format("2006-01-02 15:04:05") + ", " + util.HumanizeTime(t, Conf.Lang) ret.HSize = humanize.BytesCustomCeil(ret.Size, 2) mTime := t if updated := ial["updated"]; "" != updated { if updatedTime, err := time.ParseInLocation("20060102150405", updated, time.Local); nil == err { mTime = updatedTime } } ret.Mtime = mTime.Unix() ret.HMtime = mTime.Format("2006-01-02 15:04:05") + ", " + util.HumanizeTime(mTime, Conf.Lang) return } func (box *Box) docIAL(p string) (ret map[string]string) { name := strings.ToLower(filepath.Base(p)) if !strings.HasSuffix(name, ".sy") { return nil } ret = cache.GetDocIAL(p) if nil != ret { return ret } filePath := filepath.Join(util.DataDir, box.ID, p) data, err := filelock.ReadFile(filePath) if util.IsCorruptedSYData(data) { box.moveCorruptedData(filePath) return nil } if nil != err { logging.LogErrorf("read file [%s] failed: %s", p, err) return nil } ret = filesys.ReadDocIAL(data) if 1 > len(ret) { logging.LogWarnf("tree [%s] is corrupted", filePath) box.moveCorruptedData(filePath) return nil } cache.PutDocIAL(p, ret) return ret } func (box *Box) moveCorruptedData(filePath string) { base := filepath.Base(filePath) to := filepath.Join(util.WorkspaceDir, "corrupted", time.Now().Format("2006-01-02-150405"), box.ID, base) if copyErr := filelock.Copy(filePath, to); nil != copyErr { logging.LogErrorf("copy corrupted data file [%s] failed: %s", filePath, copyErr) return } if removeErr := filelock.Remove(filePath); nil != removeErr { logging.LogErrorf("remove corrupted data file [%s] failed: %s", filePath, removeErr) return } logging.LogWarnf("moved corrupted data file [%s] to [%s]", filePath, to) } func SearchDocsByKeyword(keyword string, flashcard bool) (ret []map[string]string) { ret = []map[string]string{} var deck *riff.Deck var deckBlockIDs []string if flashcard { deck = Decks[builtinDeckID] if nil == deck { return } deckBlockIDs = deck.GetBlockIDs() } openedBoxes := Conf.GetOpenedBoxes() boxes := map[string]*Box{} for _, box := range openedBoxes { boxes[box.ID] = box } var rootBlocks []*sql.Block if "" != keyword { for _, box := range boxes { if strings.Contains(box.Name, keyword) { if flashcard { newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs) if 0 < flashcardCount { 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)}) } } else { ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon}) } } } condition := "hpath LIKE '%" + keyword + "%'" if "" != keyword { namCondition := Conf.Search.NAMFilter(keyword) if "" != namCondition { condition += " " + namCondition } } rootBlocks = sql.QueryRootBlockByCondition(condition) } else { for _, box := range boxes { if flashcard { newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs) if 0 < flashcardCount { 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)}) } } else { ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon}) } } } for _, rootBlock := range rootBlocks { b := boxes[rootBlock.Box] if nil == b { continue } hPath := b.Name + rootBlock.HPath if flashcard { newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootBlock.ID, deck, deckBlockIDs) if 0 < flashcardCount { 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)}) } } else { ret = append(ret, map[string]string{"path": rootBlock.Path, "hPath": hPath, "box": rootBlock.Box, "boxIcon": b.Icon}) } } sort.Slice(ret, func(i, j int) bool { return ret[i]["hPath"] < ret[j]["hPath"] }) return } type FileInfo struct { path string name string size int64 isdir bool } func ListDocTree(boxID, listPath string, sortMode int, flashcard, showHidden bool, maxListCount int) (ret []*File, totals int, err error) { //os.MkdirAll("pprof", 0755) //cpuProfile, _ := os.Create("pprof/cpu_profile_list_doc_tree") //pprof.StartCPUProfile(cpuProfile) //defer pprof.StopCPUProfile() ret = []*File{} var deck *riff.Deck var deckBlockIDs []string if flashcard { deck = Decks[builtinDeckID] if nil == deck { return } deckBlockIDs = deck.GetBlockIDs() } box := Conf.Box(boxID) if nil == box { return nil, 0, errors.New(Conf.Language(0)) } boxConf := box.GetConf() if util.SortModeUnassigned == sortMode { sortMode = Conf.FileTree.Sort if util.SortModeFileTree != boxConf.SortMode { sortMode = boxConf.SortMode } } var files []*FileInfo start := time.Now() files, totals, err = box.Ls(listPath) if nil != err { return } elapsed := time.Now().Sub(start).Milliseconds() if 100 < elapsed { logging.LogWarnf("ls elapsed [%dms]", elapsed) } start = time.Now() boxLocalPath := filepath.Join(util.DataDir, box.ID) var docs []*File for _, file := range files { if file.isdir { if !ast.IsNodeIDPattern(file.name) { continue } parentDocPath := strings.TrimSuffix(file.path, "/") + ".sy" parentDocFile := box.Stat(parentDocPath) if nil == parentDocFile { continue } if ial := box.docIAL(parentDocPath); nil != ial { if !showHidden && "true" == ial["custom-hidden"] { continue } doc := box.docFromFileInfo(parentDocFile, ial) subFiles, err := os.ReadDir(filepath.Join(boxLocalPath, file.path)) if nil == err { for _, subFile := range subFiles { subDocFilePath := path.Join(file.path, subFile.Name()) if subIAL := box.docIAL(subDocFilePath); "true" == subIAL["custom-hidden"] { continue } if strings.HasSuffix(subFile.Name(), ".sy") { doc.SubFileCount++ } } } if flashcard { rootID := strings.TrimSuffix(filepath.Base(parentDocPath), ".sy") newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootID, deck, deckBlockIDs) if 0 < flashcardCount { doc.NewFlashcardCount = newFlashcardCount doc.DueFlashcardCount = dueFlashcardCount doc.FlashcardCount = flashcardCount docs = append(docs, doc) } } else { docs = append(docs, doc) } } continue } subFolder := filepath.Join(boxLocalPath, strings.TrimSuffix(file.path, ".sy")) if gulu.File.IsDir(subFolder) { continue } if ial := box.docIAL(file.path); nil != ial { if !showHidden && "true" == ial["custom-hidden"] { continue } doc := box.docFromFileInfo(file, ial) if flashcard { rootID := strings.TrimSuffix(filepath.Base(file.path), ".sy") newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootID, deck, deckBlockIDs) if 0 < flashcardCount { doc.NewFlashcardCount = newFlashcardCount doc.DueFlashcardCount = dueFlashcardCount doc.FlashcardCount = flashcardCount docs = append(docs, doc) } } else { docs = append(docs, doc) } } } elapsed = time.Now().Sub(start).Milliseconds() if 500 < elapsed { logging.LogWarnf("build docs [%d] elapsed [%dms]", len(docs), elapsed) } start = time.Now() refCount := sql.QueryRootBlockRefCount() for _, doc := range docs { if count := refCount[doc.ID]; 0 < count { doc.Count = count } } elapsed = time.Now().Sub(start).Milliseconds() if 500 < elapsed { logging.LogWarnf("query root block ref count elapsed [%dms]", elapsed) } start = time.Now() switch sortMode { case util.SortModeNameASC: sort.Slice(docs, func(i, j int) bool { return util.PinYinCompare(util.RemoveEmojiInvisible(docs[i].Name), util.RemoveEmojiInvisible(docs[j].Name)) }) case util.SortModeNameDESC: sort.Slice(docs, func(i, j int) bool { return util.PinYinCompare(util.RemoveEmojiInvisible(docs[j].Name), util.RemoveEmojiInvisible(docs[i].Name)) }) case util.SortModeUpdatedASC: sort.Slice(docs, func(i, j int) bool { return docs[i].Mtime < docs[j].Mtime }) case util.SortModeUpdatedDESC: sort.Slice(docs, func(i, j int) bool { return docs[i].Mtime > docs[j].Mtime }) case util.SortModeAlphanumASC: sort.Slice(docs, func(i, j int) bool { return natsort.Compare(util.RemoveEmojiInvisible(docs[i].Name), util.RemoveEmojiInvisible(docs[j].Name)) }) case util.SortModeAlphanumDESC: sort.Slice(docs, func(i, j int) bool { return natsort.Compare(util.RemoveEmojiInvisible(docs[j].Name), util.RemoveEmojiInvisible(docs[i].Name)) }) case util.SortModeCustom: fileTreeFiles := docs box.fillSort(&fileTreeFiles) sort.Slice(fileTreeFiles, func(i, j int) bool { if fileTreeFiles[i].Sort == fileTreeFiles[j].Sort { return util.TimeFromID(fileTreeFiles[i].ID) > util.TimeFromID(fileTreeFiles[j].ID) } return fileTreeFiles[i].Sort < fileTreeFiles[j].Sort }) ret = append(ret, fileTreeFiles...) totals = len(ret) if maxListCount < len(ret) { ret = ret[:maxListCount] } ret = ret[:] return case util.SortModeRefCountASC: sort.Slice(docs, func(i, j int) bool { return docs[i].Count < docs[j].Count }) case util.SortModeRefCountDESC: sort.Slice(docs, func(i, j int) bool { return docs[i].Count > docs[j].Count }) case util.SortModeCreatedASC: sort.Slice(docs, func(i, j int) bool { return docs[i].CTime < docs[j].CTime }) case util.SortModeCreatedDESC: sort.Slice(docs, func(i, j int) bool { return docs[i].CTime > docs[j].CTime }) case util.SortModeSizeASC: sort.Slice(docs, func(i, j int) bool { return docs[i].Size < docs[j].Size }) case util.SortModeSizeDESC: sort.Slice(docs, func(i, j int) bool { return docs[i].Size > docs[j].Size }) case util.SortModeSubDocCountASC: sort.Slice(docs, func(i, j int) bool { return docs[i].SubFileCount < docs[j].SubFileCount }) case util.SortModeSubDocCountDESC: sort.Slice(docs, func(i, j int) bool { return docs[i].SubFileCount > docs[j].SubFileCount }) } if util.SortModeCustom != sortMode { ret = append(ret, docs...) } totals = len(ret) if maxListCount < len(ret) { ret = ret[:maxListCount] } ret = ret[:] elapsed = time.Now().Sub(start).Milliseconds() if 200 < elapsed { logging.LogInfof("sort docs elapsed [%dms]", elapsed) } return } func ContentStat(content string) (ret *util.BlockStatResult) { luteEngine := util.NewLute() return contentStat(content, luteEngine) } func contentStat(content string, luteEngine *lute.Lute) (ret *util.BlockStatResult) { tree := luteEngine.BlockDOM2Tree(content) runeCnt, wordCnt, linkCnt, imgCnt, refCnt := tree.Root.Stat() return &util.BlockStatResult{ RuneCount: runeCnt, WordCount: wordCnt, LinkCount: linkCnt, ImageCount: imgCnt, RefCount: refCnt, } } func BlocksWordCount(ids []string) (ret *util.BlockStatResult) { ret = &util.BlockStatResult{} trees := filesys.LoadTrees(ids) for _, id := range ids { tree := trees[id] if nil == tree { continue } node := treenode.GetNodeInTree(tree, id) if nil == node { continue } runeCnt, wordCnt, linkCnt, imgCnt, refCnt := node.Stat() ret.RuneCount += runeCnt ret.WordCount += wordCnt ret.LinkCount += linkCnt ret.ImageCount += imgCnt ret.RefCount += refCnt } return } func StatTree(id string) (ret *util.BlockStatResult) { WaitForWritingFiles() tree, _ := LoadTreeByBlockID(id) if nil == tree { return } var databaseBlockNodes []*ast.Node ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering || ast.NodeAttributeView != n.Type { return ast.WalkContinue } databaseBlockNodes = append(databaseBlockNodes, n) return ast.WalkContinue }) luteEngine := util.NewLute() var dbRuneCnt, dbWordCnt, dbLinkCnt, dbImgCnt, dbRefCnt int for _, n := range databaseBlockNodes { if "" == n.AttributeViewID { continue } attrView, _ := av.ParseAttributeView(n.AttributeViewID) if nil == attrView { continue } content := bytes.Buffer{} for _, kValues := range attrView.KeyValues { for _, v := range kValues.Values { switch kValues.Key.Type { case av.KeyTypeURL: if v.IsEmpty() { continue } dbLinkCnt++ content.WriteString(v.URL.Content) case av.KeyTypeMAsset: if v.IsEmpty() { continue } for _, asset := range v.MAsset { if av.AssetTypeImage == asset.Type { dbImgCnt++ } } case av.KeyTypeBlock: if v.IsEmpty() { continue } if !v.IsDetached { dbRefCnt++ } content.WriteString(v.Block.Content) case av.KeyTypeText: if v.IsEmpty() { continue } content.WriteString(v.Text.Content) case av.KeyTypeNumber: if v.IsEmpty() { continue } v.Number.FormatNumber() content.WriteString(v.Number.FormattedContent) case av.KeyTypeEmail: if v.IsEmpty() { continue } content.WriteString(v.Email.Content) case av.KeyTypePhone: if v.IsEmpty() { continue } content.WriteString(v.Phone.Content) } } } dbStat := contentStat(content.String(), luteEngine) dbRuneCnt += dbStat.RuneCount dbWordCnt += dbStat.WordCount } runeCnt, wordCnt, linkCnt, imgCnt, refCnt := tree.Root.Stat() runeCnt += dbRuneCnt wordCnt += dbWordCnt linkCnt += dbLinkCnt imgCnt += dbImgCnt refCnt += dbRefCnt return &util.BlockStatResult{ RuneCount: runeCnt, WordCount: wordCnt, LinkCount: linkCnt, ImageCount: imgCnt, RefCount: refCnt, } } func GetDoc(startID, endID, id string, index int, query string, queryTypes map[string]bool, queryMethod, mode int, size int, isBacklink bool) (blockCount int, dom, parentID, parent2ID, rootID, typ string, eof, scroll bool, boxID, docPath string, isBacklinkExpand bool, err error) { //os.MkdirAll("pprof", 0755) //cpuProfile, _ := os.Create("pprof/GetDoc") //pprof.StartCPUProfile(cpuProfile) //defer pprof.StopCPUProfile() WaitForWritingFiles() // 写入数据时阻塞,避免获取到的数据不一致 inputIndex := index tree, err := LoadTreeByBlockID(id) if nil != err { if ErrBlockNotFound == err { if 0 == mode { err = ErrTreeNotFound // 初始化打开文档时如果找不到则关闭编辑器 } } return } if nil == tree { err = ErrBlockNotFound return } luteEngine := NewLute() node := treenode.GetNodeInTree(tree, id) if nil == node { // Unable to open the doc when the block pointed by the scroll position does not exist https://github.com/siyuan-note/siyuan/issues/9030 node = treenode.GetNodeInTree(tree, tree.Root.ID) if nil == node { err = ErrBlockNotFound return } } if isBacklink { // 引用计数浮窗请求,需要按照反链逻辑组装 https://github.com/siyuan-note/siyuan/issues/6853 if ast.NodeParagraph == node.Type { if nil != node.Parent && ast.NodeListItem == node.Parent.Type { node = node.Parent } } } located := false isDoc := ast.NodeDocument == node.Type isHeading := ast.NodeHeading == node.Type boxID = node.Box docPath = node.Path if isDoc { if 4 == mode { // 加载文档末尾 node = node.LastChild located = true // 重新计算 index ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue } index++ return ast.WalkContinue }) } else { node = node.FirstChild } typ = ast.NodeDocument.String() idx := 0 if 0 < index { ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering || !n.IsChildBlockOf(tree.Root, 1) { return ast.WalkContinue } idx++ if index == idx { node = n.DocChild() if "1" == node.IALAttr("heading-fold") { // 加载到折叠标题下方块的话需要回溯到上方标题块 for h := node.Previous; nil != h; h = h.Previous { if "1" == h.IALAttr("fold") { node = h break } } } located = true return ast.WalkStop } return ast.WalkContinue }) } } else { if 0 == index && 0 != mode { // 非文档且没有指定 index 时需要计算 index ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue } index++ if id == n.ID { node = n.DocChild() located = true return ast.WalkStop } return ast.WalkContinue }) } } if 1 < index && !located { count := 0 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue } count++ if index == count { node = n.DocChild() return ast.WalkStop } return ast.WalkContinue }) } blockCount = tree.DocBlockCount() if ast.NodeDocument == node.Type { parentID = node.ID parent2ID = parentID } else { parentID = node.Parent.ID parent2ID = parentID tmp := node if ast.NodeListItem == node.Type { // 列表项聚焦返回和面包屑保持一致 https://github.com/siyuan-note/siyuan/issues/4914 tmp = node.Parent } if headingParent := treenode.HeadingParent(tmp); nil != headingParent { parent2ID = headingParent.ID } } rootID = tree.Root.ID if !isDoc { typ = node.Type.String() } // 判断是否需要显示动态加载滚动条 https://github.com/siyuan-note/siyuan/issues/7693 childCount := 0 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue } if 1 > childCount { childCount = 1 } else { childCount += treenode.CountBlockNodes(n) } if childCount > Conf.Editor.DynamicLoadBlocks { scroll = true return ast.WalkStop } return ast.WalkContinue }) var nodes []*ast.Node if isBacklink { // 引用计数浮窗请求,需要按照反链逻辑组装 https://github.com/siyuan-note/siyuan/issues/6853 nodes, isBacklinkExpand = getBacklinkRenderNodes(node) } else { // 如果同时存在 startID 和 endID,并且是动态加载的情况,则只加载 startID 和 endID 之间的块 [startID, endID] if "" != startID && "" != endID && scroll { nodes, eof = loadNodesByStartEnd(tree, startID, endID) if 1 > len(nodes) { // 按 mode 加载兜底 nodes, eof = loadNodesByMode(node, inputIndex, mode, size, isDoc, isHeading) } else { // 文档块没有指定 index 时需要计算 index,否则初次打开文档时 node-index 会为 0,导致首次 Ctrl+Home 无法回到顶部 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue } index++ if nodes[0].ID == n.ID { return ast.WalkStop } return ast.WalkContinue }) } } else { nodes, eof = loadNodesByMode(node, inputIndex, mode, size, isDoc, isHeading) } } refCount := sql.QueryRootChildrenRefCount(rootID) virtualBlockRefKeywords := getBlockVirtualRefKeywords(tree.Root) subTree := &parse.Tree{ID: rootID, Root: &ast.Node{Type: ast.NodeDocument}, Marks: tree.Marks} var keywords []string if "" != query && (0 == queryMethod || 1 == queryMethod) { // 只有关键字搜索和查询语法搜索才支持高亮 if 0 == queryMethod { query = stringQuery(query) } typeFilter := buildTypeFilter(queryTypes) keywords = highlightByQuery(query, typeFilter, rootID) } for _, n := range nodes { var unlinks []*ast.Node ast.Walk(n, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue } if "1" == n.IALAttr("heading-fold") { // 折叠标题下被引用的块无法悬浮查看 // The referenced block under the folded heading cannot be hovered to view https://github.com/siyuan-note/siyuan/issues/9582 if (0 != mode && id != n.ID) || isDoc { unlinks = append(unlinks, n) return ast.WalkContinue } } if avs := n.IALAttr(av.NodeAttrNameAvs); "" != avs { // 填充属性视图角标 Display the database title on the block superscript https://github.com/siyuan-note/siyuan/issues/10545 avNames := getAvNames(n.IALAttr(av.NodeAttrNameAvs)) if "" != avNames { n.SetIALAttr(av.NodeAttrViewNames, avNames) } } if "" != n.ID { // 填充块引计数 if cnt := refCount[n.ID]; 0 < cnt { n.SetIALAttr("refcount", strconv.Itoa(cnt)) } } if 0 < len(keywords) { hitBlock := false for p := n.Parent; nil != p; p = p.Parent { if p.ID == id { hitBlock = true break } } if hitBlock { if ast.NodeCodeBlockCode == n.Type && !treenode.IsChartCodeBlockCode(n) { // 支持代码块搜索定位 https://github.com/siyuan-note/siyuan/issues/5520 code := string(n.Tokens) markedCode := search.EncloseHighlighting(code, keywords, search.SearchMarkLeft, search.SearchMarkRight, Conf.Search.CaseSensitive, false) if code != markedCode { n.Tokens = gulu.Str.ToBytes(markedCode) return ast.WalkContinue } } else if markReplaceSpan(n, &unlinks, keywords, search.MarkDataType, luteEngine) { return ast.WalkContinue } } } if processVirtualRef(n, &unlinks, virtualBlockRefKeywords, refCount, luteEngine) { return ast.WalkContinue } return ast.WalkContinue }) for _, unlink := range unlinks { unlink.Unlink() } subTree.Root.AppendChild(n) } luteEngine.RenderOptions.NodeIndexStart = index dom = luteEngine.Tree2BlockDOM(subTree, luteEngine.RenderOptions) go setRecentDocByTree(tree) return } func loadNodesByStartEnd(tree *parse.Tree, startID, endID string) (nodes []*ast.Node, eof bool) { node := treenode.GetNodeInTree(tree, startID) if nil == node { return } nodes = append(nodes, node) for n := node.Next; nil != n; n = n.Next { if treenode.IsInFoldedHeading(n, nil) { continue } nodes = append(nodes, n) if n.ID == endID { if next := n.Next; nil == next { eof = true } else { eof = util2.IsDocIAL(n.Tokens) || util2.IsDocIAL(next.Tokens) } break } } return } func loadNodesByMode(node *ast.Node, inputIndex, mode, size int, isDoc, isHeading bool) (nodes []*ast.Node, eof bool) { if 2 == mode /* 向下 */ { next := node.Next if ast.NodeHeading == node.Type && "1" == node.IALAttr("fold") { // 标题展开时进行动态加载导致重复内容 https://github.com/siyuan-note/siyuan/issues/4671 // 这里要考虑折叠标题是最后一个块的情况 if children := treenode.HeadingChildren(node); 0 < len(children) { next = children[len(children)-1].Next } } if nil == next { eof = true } else { eof = util2.IsDocIAL(node.Tokens) || util2.IsDocIAL(next.Tokens) } } count := 0 switch mode { case 0: // 仅加载当前 ID nodes = append(nodes, node) if isDoc { for n := node.Next; nil != n; n = n.Next { if treenode.IsInFoldedHeading(n, nil) { continue } nodes = append(nodes, n) if 1 > count { count++ } else { count += treenode.CountBlockNodes(n) } if size < count { break } } } else if isHeading { level := node.HeadingLevel for n := node.Next; nil != n; n = n.Next { if treenode.IsInFoldedHeading(n, node) { // 大纲点击折叠标题跳转聚焦 https://github.com/siyuan-note/siyuan/issues/4920 // 多级标题折叠后上级块引浮窗中未折叠 https://github.com/siyuan-note/siyuan/issues/4997 continue } if ast.NodeHeading == n.Type { if n.HeadingLevel <= level { break } } nodes = append(nodes, n) count++ if size < count { break } } } case 4: // Ctrl+End 跳转到末尾后向上加载 for n := node; nil != n; n = n.Previous { if treenode.IsInFoldedHeading(n, nil) { continue } nodes = append([]*ast.Node{n}, nodes...) if 1 > count { count++ } else { count += treenode.CountBlockNodes(n) } if size < count { break } } eof = true case 1: // 向上加载 for n := node.Previous; /* 从上一个节点开始加载 */ nil != n; n = n.Previous { if treenode.IsInFoldedHeading(n, nil) { continue } nodes = append([]*ast.Node{n}, nodes...) if 1 > count { count++ } else { count += treenode.CountBlockNodes(n) } if size < count { break } } eof = nil == node.Previous case 2: // 向下加载 for n := node.Next; /* 从下一个节点开始加载 */ nil != n; n = n.Next { if treenode.IsInFoldedHeading(n, node) { continue } nodes = append(nodes, n) if 1 > count { count++ } else { count += treenode.CountBlockNodes(n) } if size < count { break } } case 3: // 上下都加载 for n := node; nil != n; n = n.Previous { if treenode.IsInFoldedHeading(n, nil) { continue } nodes = append([]*ast.Node{n}, nodes...) if 1 > count { count++ } else { count += treenode.CountBlockNodes(n) } if 0 < inputIndex { if 1 < count { break // 滑块指示器加载 } } else { if size < count { break } } } if size/2 < count { size = size / 2 } else { size = size - count } count = 0 for n := node.Next; nil != n; n = n.Next { if treenode.IsInFoldedHeading(n, nil) { continue } nodes = append(nodes, n) if 1 > count { count++ } else { count += treenode.CountBlockNodes(n) } if 0 < inputIndex { if size < count { break } } else { if size < count { break } } } } return } func writeTreeUpsertQueue(tree *parse.Tree) (err error) { if err = filesys.WriteTree(tree); nil != err { return } sql.UpsertTreeQueue(tree) return } func writeTreeIndexQueue(tree *parse.Tree) (err error) { if err = filesys.WriteTree(tree); nil != err { return } sql.IndexTreeQueue(tree) return } func indexWriteTreeIndexQueue(tree *parse.Tree) (err error) { treenode.IndexBlockTree(tree) return writeTreeIndexQueue(tree) } func indexWriteTreeUpsertQueue(tree *parse.Tree) (err error) { treenode.UpsertBlockTree(tree) return writeTreeUpsertQueue(tree) } func renameWriteJSONQueue(tree *parse.Tree) (err error) { if err = filesys.WriteTree(tree); nil != err { return } sql.RenameTreeQueue(tree) treenode.UpsertBlockTree(tree) return } func DuplicateDoc(tree *parse.Tree) { msgId := util.PushMsg(Conf.Language(116), 30000) defer util.PushClearMsg(msgId) previousID := tree.Root.ID resetTree(tree, "Duplicated", false) createTreeTx(tree) WaitForWritingFiles() // 复制为副本时将该副本块插入到数据库中 https://github.com/siyuan-note/siyuan/issues/11959 avs := tree.Root.IALAttr(av.NodeAttrNameAvs) for _, avID := range strings.Split(avs, ",") { if !ast.IsNodeIDPattern(avID) { continue } AddAttributeViewBlock(nil, []map[string]interface{}{{ "id": tree.Root.ID, "isDetached": false, }}, avID, "", previousID, false) util.PushReloadAttrView(avID) } return } func createTreeTx(tree *parse.Tree) { transaction := &Transaction{DoOperations: []*Operation{{Action: "create", Data: tree}}} PerformTransactions(&[]*Transaction{transaction}) } var createDocLock = sync.Mutex{} func CreateDocByMd(boxID, p, title, md string, sorts []string) (tree *parse.Tree, err error) { createDocLock.Lock() defer createDocLock.Unlock() box := Conf.Box(boxID) if nil == box { err = errors.New(Conf.Language(0)) return } luteEngine := util.NewLute() dom := luteEngine.Md2BlockDOM(md, false) tree, err = createDoc(box.ID, p, title, dom) if nil != err { return } WaitForWritingFiles() ChangeFileTreeSort(box.ID, sorts) return } func CreateWithMarkdown(boxID, hPath, md, parentID, id string, withMath bool) (retID string, err error) { createDocLock.Lock() defer createDocLock.Unlock() box := Conf.Box(boxID) if nil == box { err = errors.New(Conf.Language(0)) return } WaitForWritingFiles() luteEngine := util.NewLute() if withMath { luteEngine.SetInlineMath(true) } dom := luteEngine.Md2BlockDOM(md, false) retID, err = createDocsByHPath(box.ID, hPath, dom, parentID, id) WaitForWritingFiles() return } func CreateDailyNote(boxID string) (p string, existed bool, err error) { createDocLock.Lock() defer createDocLock.Unlock() box := Conf.Box(boxID) if nil == box { err = ErrBoxNotFound return } boxConf := box.GetConf() if "" == boxConf.DailyNoteSavePath || "/" == boxConf.DailyNoteSavePath { err = errors.New(Conf.Language(49)) return } hPath, err := RenderGoTemplate(boxConf.DailyNoteSavePath) if nil != err { return } WaitForWritingFiles() existRoot := treenode.GetBlockTreeRootByHPath(box.ID, hPath) if nil != existRoot { existed = true p = existRoot.Path tree, loadErr := LoadTreeByBlockID(existRoot.RootID) if nil != loadErr { logging.LogWarnf("load tree by block id [%s] failed: %v", existRoot.RootID, loadErr) return } p = tree.Path date := time.Now().Format("20060102") if tree.Root.IALAttr("custom-dailynote-"+date) == "" { tree.Root.SetIALAttr("custom-dailynote-"+date, date) if err = indexWriteTreeUpsertQueue(tree); nil != err { return } } return } id, err := createDocsByHPath(box.ID, hPath, "", "", "") if nil != err { return } var templateTree *parse.Tree var templateDom string if "" != boxConf.DailyNoteTemplatePath { tplPath := filepath.Join(util.DataDir, "templates", boxConf.DailyNoteTemplatePath) if !filelock.IsExist(tplPath) { logging.LogWarnf("not found daily note template [%s]", tplPath) } else { var renderErr error templateTree, templateDom, renderErr = RenderTemplate(tplPath, id, false) if nil != renderErr { logging.LogWarnf("render daily note template [%s] failed: %s", boxConf.DailyNoteTemplatePath, err) } } } if "" != templateDom { var tree *parse.Tree tree, err = LoadTreeByBlockID(id) if nil == err { tree.Root.FirstChild.Unlink() luteEngine := util.NewLute() newTree := luteEngine.BlockDOM2Tree(templateDom) var children []*ast.Node for c := newTree.Root.FirstChild; nil != c; c = c.Next { children = append(children, c) } for _, c := range children { tree.Root.AppendChild(c) } // Creating a dailynote template supports doc attributes https://github.com/siyuan-note/siyuan/issues/10698 templateIALs := parse.IAL2Map(templateTree.Root.KramdownIAL) for k, v := range templateIALs { if "name" == k || "alias" == k || "bookmark" == k || "memo" == k || strings.HasPrefix(k, "custom-") { tree.Root.SetIALAttr(k, v) } } tree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr()) if err = indexWriteTreeUpsertQueue(tree); nil != err { return } } } IncSync() WaitForWritingFiles() tree, err := LoadTreeByBlockID(id) if nil != err { logging.LogErrorf("load tree by block id [%s] failed: %v", id, err) return } p = tree.Path date := time.Now().Format("20060102") tree.Root.SetIALAttr("custom-dailynote-"+date, date) if err = indexWriteTreeUpsertQueue(tree); nil != err { return } return } func GetHPathByPath(boxID, p string) (hPath string, err error) { if "/" == p { hPath = "/" return } luteEngine := util.NewLute() tree, err := filesys.LoadTree(boxID, p, luteEngine) if nil != err { return } hPath = tree.HPath return } func GetHPathsByPaths(paths []string) (hPaths []string, err error) { pathsBoxes := getBoxesByPaths(paths) for p, box := range pathsBoxes { if nil == box { logging.LogWarnf("box not found by path [%s]", p) continue } bt := treenode.GetBlockTreeByPath(p) if nil == bt { logging.LogWarnf("block tree not found by path [%s]", p) continue } hpath := html.UnescapeString(bt.HPath) hPaths = append(hPaths, box.Name+hpath) } return } func GetHPathByID(id string) (hPath string, err error) { tree, err := LoadTreeByBlockID(id) if nil != err { return } hPath = tree.HPath return } func GetFullHPathByID(id string) (hPath string, err error) { tree, err := LoadTreeByBlockID(id) if nil != err { return } box := Conf.Box(tree.Box) if nil == box { err = ErrBoxNotFound return } hPath = box.Name + tree.HPath return } func GetIDsByHPath(hpath, boxID string) (ret []string, err error) { ret = []string{} roots := treenode.GetBlockTreeRootsByHPath(boxID, hpath) if 1 > len(roots) { return } for _, root := range roots { ret = append(ret, root.ID) } ret = gulu.Str.RemoveDuplicatedElem(ret) if 1 > len(ret) { ret = []string{} } return } func MoveDocs(fromPaths []string, toBoxID, toPath string, callback interface{}) (err error) { toBox := Conf.Box(toBoxID) if nil == toBox { err = errors.New(Conf.Language(0)) return } fromPaths = util.FilterMoveDocFromPaths(fromPaths, toPath) if 1 > len(fromPaths) { return } pathsBoxes := getBoxesByPaths(fromPaths) if 1 == len(fromPaths) { // 移动到自己的父文档下的情况相当于不移动,直接返回 if fromBox := pathsBoxes[fromPaths[0]]; nil != fromBox && fromBox.ID == toBoxID { parentDir := path.Dir(fromPaths[0]) if ("/" == toPath && "/" == parentDir) || (parentDir+".sy" == toPath) { return } } } // 检查路径深度是否超过限制 for fromPath, fromBox := range pathsBoxes { childDepth := util.GetChildDocDepth(filepath.Join(util.DataDir, fromBox.ID, fromPath)) if depth := strings.Count(toPath, "/") + childDepth; 6 < depth && !Conf.FileTree.AllowCreateDeeper { err = errors.New(Conf.Language(118)) return } } // A progress layer appears when moving more than 64 documents at once https://github.com/siyuan-note/siyuan/issues/9356 subDocsCount := 0 for fromPath, fromBox := range pathsBoxes { subDocsCount += countSubDocs(fromBox.ID, fromPath) } needShowProgress := 64 < subDocsCount if needShowProgress { defer util.PushClearProgress() } WaitForWritingFiles() luteEngine := util.NewLute() count := 0 for fromPath, fromBox := range pathsBoxes { count++ if needShowProgress { util.PushEndlessProgress(fmt.Sprintf(Conf.Language(70), fmt.Sprintf("%d/%d", count, len(fromPaths)))) } _, err = moveDoc(fromBox, fromPath, toBox, toPath, luteEngine, callback) if nil != err { return } } cache.ClearDocsIAL() IncSync() return } func countSubDocs(box, p string) (ret int) { p = strings.TrimSuffix(p, ".sy") _ = filepath.Walk(filepath.Join(util.DataDir, box, p), func(path string, info os.FileInfo, err error) error { if nil != err { return err } if info.IsDir() { return nil } if strings.HasSuffix(path, ".sy") { ret++ } return nil }) return } func moveDoc(fromBox *Box, fromPath string, toBox *Box, toPath string, luteEngine *lute.Lute, callback interface{}) (newPath string, err error) { isSameBox := fromBox.ID == toBox.ID if isSameBox { if !fromBox.Exist(toPath) { err = ErrBlockNotFound return } } else { if !toBox.Exist(toPath) { err = ErrBlockNotFound return } } tree, err := filesys.LoadTree(fromBox.ID, fromPath, luteEngine) if nil != err { err = ErrBlockNotFound return } moveToRoot := "/" == toPath toBlockID := tree.ID fromFolder := path.Join(path.Dir(fromPath), tree.ID) toFolder := "/" if !moveToRoot { var toTree *parse.Tree if isSameBox { toTree, err = filesys.LoadTree(fromBox.ID, toPath, luteEngine) } else { toTree, err = filesys.LoadTree(toBox.ID, toPath, luteEngine) } if nil != err { err = ErrBlockNotFound return } toBlockID = toTree.ID toFolder = path.Join(path.Dir(toPath), toBlockID) } if isSameBox { if err = fromBox.MkdirAll(toFolder); nil != err { return } } else { if err = toBox.MkdirAll(toFolder); nil != err { return } } needMoveSubDocs := fromBox.Exist(fromFolder) if needMoveSubDocs { // 移动子文档文件夹 newFolder := path.Join(toFolder, tree.ID) if isSameBox { if err = fromBox.Move(fromFolder, newFolder); nil != err { return } } else { absFromPath := filepath.Join(util.DataDir, fromBox.ID, fromFolder) absToPath := filepath.Join(util.DataDir, toBox.ID, newFolder) if filelock.IsExist(absToPath) { filelock.Remove(absToPath) } if err = filelock.Rename(absFromPath, absToPath); nil != err { msg := fmt.Sprintf(Conf.Language(5), fromBox.Name, fromPath, err) logging.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, fromBox.ID, err) err = errors.New(msg) return } } } newPath = path.Join(toFolder, tree.ID+".sy") if isSameBox { if err = fromBox.Move(fromPath, newPath); nil != err { return } tree, err = filesys.LoadTree(fromBox.ID, newPath, luteEngine) if nil != err { return } moveTree(tree) } else { absFromPath := filepath.Join(util.DataDir, fromBox.ID, fromPath) absToPath := filepath.Join(util.DataDir, toBox.ID, newPath) if err = filelock.Rename(absFromPath, absToPath); nil != err { msg := fmt.Sprintf(Conf.Language(5), fromBox.Name, fromPath, err) logging.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, fromBox.ID, err) err = errors.New(msg) return } tree, err = filesys.LoadTree(toBox.ID, newPath, luteEngine) if nil != err { return } moveTree(tree) moveSorts(tree.ID, fromBox.ID, toBox.ID) } if needMoveSubDocs { // 将其所有子文档的移动事件推送到前端 https://github.com/siyuan-note/siyuan/issues/11661 subDocsFolder := path.Join(toFolder, tree.ID) syFiles := listSyFiles(path.Join(toBox.ID, subDocsFolder)) for _, syFile := range syFiles { relPath := strings.TrimPrefix(syFile, "/"+path.Join(toBox.ID, toFolder)) subFromPath := path.Join(path.Dir(fromPath), relPath) subToPath := path.Join(toFolder, relPath) evt := util.NewCmdResult("moveDoc", 0, util.PushModeBroadcast) evt.Data = map[string]interface{}{ "fromNotebook": fromBox.ID, "fromPath": subFromPath, "toNotebook": toBox.ID, "toPath": path.Dir(subToPath) + ".sy", "newPath": subToPath, } evt.Callback = callback util.PushEvent(evt) } } evt := util.NewCmdResult("moveDoc", 0, util.PushModeBroadcast) evt.Data = map[string]interface{}{ "fromNotebook": fromBox.ID, "fromPath": fromPath, "toNotebook": toBox.ID, "toPath": toPath, "newPath": newPath, } evt.Callback = callback util.PushEvent(evt) return } func RemoveDoc(boxID, p string) { box := Conf.Box(boxID) if nil == box { return } WaitForWritingFiles() luteEngine := util.NewLute() removeDoc(box, p, luteEngine) IncSync() return } func RemoveDocs(paths []string) { util.PushEndlessProgress(Conf.Language(116)) defer util.PushClearProgress() paths = util.FilterSelfChildDocs(paths) pathsBoxes := getBoxesByPaths(paths) WaitForWritingFiles() luteEngine := util.NewLute() for p, box := range pathsBoxes { removeDoc(box, p, luteEngine) } return } func removeDoc(box *Box, p string, luteEngine *lute.Lute) { tree, _ := filesys.LoadTree(box.ID, p, luteEngine) if nil == tree { return } historyDir, err := GetHistoryDir(HistoryOpDelete) if nil != err { logging.LogErrorf("get history dir failed: %s", err) return } historyPath := filepath.Join(historyDir, box.ID, p) absPath := filepath.Join(util.DataDir, box.ID, p) if err = filelock.Copy(absPath, historyPath); nil != err { logging.LogErrorf("backup [path=%s] to history [%s] failed: %s", absPath, historyPath, err) return } // 关联的属性视图也要复制到历史中 https://github.com/siyuan-note/siyuan/issues/9567 avNodes := tree.Root.ChildrenByType(ast.NodeAttributeView) for _, avNode := range avNodes { srcAvPath := filepath.Join(util.DataDir, "storage", "av", avNode.AttributeViewID+".json") destAvPath := filepath.Join(historyDir, "storage", "av", avNode.AttributeViewID+".json") if copyErr := filelock.Copy(srcAvPath, destAvPath); nil != copyErr { logging.LogErrorf("copy av [%s] failed: %s", srcAvPath, copyErr) } } copyDocAssetsToDataAssets(box.ID, p) removeIDs := treenode.RootChildIDs(tree.ID) dir := path.Dir(p) childrenDir := path.Join(dir, tree.ID) existChildren := box.Exist(childrenDir) if existChildren { absChildrenDir := filepath.Join(util.DataDir, tree.Box, childrenDir) historyPath = filepath.Join(historyDir, tree.Box, childrenDir) if err = filelock.Copy(absChildrenDir, historyPath); nil != err { logging.LogErrorf("backup [path=%s] to history [%s] failed: %s", absChildrenDir, historyPath, err) return } } indexHistoryDir(filepath.Base(historyDir), util.NewLute()) allRemoveRootIDs := []string{tree.ID} allRemoveRootIDs = append(allRemoveRootIDs, removeIDs...) for _, rootID := range allRemoveRootIDs { removeTree, _ := LoadTreeByBlockID(rootID) if nil == removeTree { continue } syncDelete2AttributeView(removeTree.Root) syncDelete2Block(removeTree.Root) } if existChildren { if err = box.Remove(childrenDir); nil != err { logging.LogErrorf("remove children dir [%s%s] failed: %s", box.ID, childrenDir, err) return } logging.LogInfof("removed children dir [%s%s]", box.ID, childrenDir) } if err = box.Remove(p); nil != err { logging.LogErrorf("remove [%s%s] failed: %s", box.ID, p, err) return } logging.LogInfof("removed doc [%s%s]", box.ID, p) box.removeSort(removeIDs) RemoveRecentDoc(removeIDs) if "/" != dir { others, err := os.ReadDir(filepath.Join(util.DataDir, box.ID, dir)) if nil == err && 1 > len(others) { box.Remove(dir) } } evt := util.NewCmdResult("removeDoc", 0, util.PushModeBroadcast) evt.Data = map[string]interface{}{ "ids": removeIDs, } util.PushEvent(evt) task.AppendTask(task.DatabaseIndex, removeDoc0, box, p, childrenDir) } func removeDoc0(box *Box, p, childrenDir string) { treenode.RemoveBlockTreesByPathPrefix(childrenDir) sql.RemoveTreePathQueue(box.ID, childrenDir) cache.RemoveDocIAL(p) return } func RenameDoc(boxID, p, title string) (err error) { box := Conf.Box(boxID) if nil == box { err = errors.New(Conf.Language(0)) return } WaitForWritingFiles() luteEngine := util.NewLute() tree, err := filesys.LoadTree(box.ID, p, luteEngine) if nil != err { return } title = removeInvisibleCharsInTitle(title) if 512 < utf8.RuneCountInString(title) { // 限制笔记本名和文档名最大长度为 `512` https://github.com/siyuan-note/siyuan/issues/6299 return errors.New(Conf.Language(106)) } oldTitle := tree.Root.IALAttr("title") if oldTitle == title { return } if "" == title { title = Conf.language(105) } title = strings.ReplaceAll(title, "/", "") tree.HPath = path.Join(path.Dir(tree.HPath), title) tree.Root.SetIALAttr("title", title) tree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr()) if err = renameWriteJSONQueue(tree); nil != err { return } refText := getNodeRefText(tree.Root) evt := util.NewCmdResult("rename", 0, util.PushModeBroadcast) evt.Data = map[string]interface{}{ "box": boxID, "id": tree.Root.ID, "path": p, "title": title, "refText": refText, } util.PushEvent(evt) box.renameSubTrees(tree) updateRefTextRenameDoc(tree) IncSync() return } func createDoc(boxID, p, title, dom string) (tree *parse.Tree, err error) { title = removeInvisibleCharsInTitle(title) if 512 < utf8.RuneCountInString(title) { // 限制笔记本名和文档名最大长度为 `512` https://github.com/siyuan-note/siyuan/issues/6299 err = errors.New(Conf.Language(106)) return } title = strings.ReplaceAll(title, "/", "") title = strings.TrimSpace(title) if "" == title { title = Conf.Language(105) } baseName := strings.TrimSpace(path.Base(p)) if "" == strings.TrimSuffix(baseName, ".sy") { err = errors.New(Conf.Language(16)) return } if strings.HasPrefix(baseName, ".") { err = errors.New(Conf.Language(13)) return } box := Conf.Box(boxID) if nil == box { err = errors.New(Conf.Language(0)) return } id := strings.TrimSuffix(path.Base(p), ".sy") var hPath string folder := path.Dir(p) if "/" != folder { parentID := path.Base(folder) parentTree, loadErr := LoadTreeByBlockID(parentID) if nil != loadErr { logging.LogErrorf("get parent tree [%s] failed", parentID) err = ErrBlockNotFound return } hPath = path.Join(parentTree.HPath, title) } else { hPath = "/" + title } if depth := strings.Count(p, "/"); 7 < depth && !Conf.FileTree.AllowCreateDeeper { err = errors.New(Conf.Language(118)) return } if !box.Exist(folder) { if err = box.MkdirAll(folder); nil != err { return } } if box.Exist(p) { err = errors.New(Conf.Language(1)) return } luteEngine := util.NewLute() tree = luteEngine.BlockDOM2Tree(dom) tree.Box = boxID tree.Path = p tree.HPath = hPath tree.ID = id tree.Root.ID = id tree.Root.Spec = "1" updated := util.TimeFromID(id) tree.Root.KramdownIAL = [][]string{{"id", id}, {"title", html.EscapeAttrVal(title)}, {"updated", updated}} if nil == tree.Root.FirstChild { tree.Root.AppendChild(treenode.NewParagraph()) } // 如果段落块中仅包含一个 mp3/mp4 超链接,则将其转换为音视频块 // Convert mp3 and mp4 hyperlinks to audio and video when moving cloud inbox to docs https://github.com/siyuan-note/siyuan/issues/9778 var unlinks []*ast.Node ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue } if ast.NodeParagraph == n.Type { link := n.FirstChild if nil != link && link.IsTextMarkType("a") { if strings.HasSuffix(link.TextMarkAHref, ".mp3") { unlinks = append(unlinks, n) audio := &ast.Node{ID: n.ID, Type: ast.NodeAudio, Tokens: []byte("")} audio.SetIALAttr("id", n.ID) audio.SetIALAttr("updated", util.TimeFromID(n.ID)) n.InsertBefore(audio) } else if strings.HasSuffix(link.TextMarkAHref, ".mp4") { unlinks = append(unlinks, n) video := &ast.Node{ID: n.ID, Type: ast.NodeVideo, Tokens: []byte("")} video.SetIALAttr("id", n.ID) video.SetIALAttr("updated", util.TimeFromID(n.ID)) n.InsertBefore(video) } } } return ast.WalkContinue }) for _, unlink := range unlinks { unlink.Unlink() } transaction := &Transaction{DoOperations: []*Operation{{Action: "create", Data: tree}}} PerformTransactions(&[]*Transaction{transaction}) WaitForWritingFiles() return } func removeInvisibleCharsInTitle(title string) string { // 不要踢掉 零宽连字符,否则有的 Emoji 会变形 https://github.com/siyuan-note/siyuan/issues/11480 title = strings.ReplaceAll(title, string(gulu.ZWJ), "__@ZWJ@__") title = gulu.Str.RemoveInvisible(title) title = strings.ReplaceAll(title, "__@ZWJ@__", string(gulu.ZWJ)) return title } func moveSorts(rootID, fromBox, toBox string) { root := treenode.GetBlockTree(rootID) if nil == root { return } fromRootSorts := map[string]int{} ids := treenode.RootChildIDs(rootID) fromConfPath := filepath.Join(util.DataDir, fromBox, ".siyuan", "sort.json") fromFullSortIDs := map[string]int{} if filelock.IsExist(fromConfPath) { data, err := filelock.ReadFile(fromConfPath) if nil != err { logging.LogErrorf("read sort conf failed: %s", err) return } if err = gulu.JSON.UnmarshalJSON(data, &fromFullSortIDs); nil != err { logging.LogErrorf("unmarshal sort conf failed: %s", err) } } for _, id := range ids { fromRootSorts[id] = fromFullSortIDs[id] } toConfPath := filepath.Join(util.DataDir, toBox, ".siyuan", "sort.json") toFullSortIDs := map[string]int{} if filelock.IsExist(toConfPath) { data, err := filelock.ReadFile(toConfPath) if nil != err { logging.LogErrorf("read sort conf failed: %s", err) return } if err = gulu.JSON.UnmarshalJSON(data, &toFullSortIDs); nil != err { logging.LogErrorf("unmarshal sort conf failed: %s", err) return } } for id, sortVal := range fromRootSorts { toFullSortIDs[id] = sortVal } data, err := gulu.JSON.MarshalJSON(toFullSortIDs) if nil != err { logging.LogErrorf("marshal sort conf failed: %s", err) return } if err = filelock.WriteFile(toConfPath, data); nil != err { logging.LogErrorf("write sort conf failed: %s", err) return } } func ChangeFileTreeSort(boxID string, paths []string) { if 1 > len(paths) { return } WaitForWritingFiles() box := Conf.Box(boxID) sortIDs := map[string]int{} max := 0 for i, p := range paths { id := strings.TrimSuffix(path.Base(p), ".sy") sortIDs[id] = i + 1 if i == len(paths)-1 { max = i + 2 } } p := paths[0] parentPath := path.Dir(p) absParentPath := filepath.Join(util.DataDir, boxID, parentPath) files, err := os.ReadDir(absParentPath) if nil != err { logging.LogErrorf("read dir [%s] failed: %s", absParentPath, err) } sortFolderIDs := map[string]int{} for _, f := range files { if !strings.HasSuffix(f.Name(), ".sy") { continue } id := strings.TrimSuffix(f.Name(), ".sy") val := sortIDs[id] if 0 == val { val = max max++ } sortFolderIDs[id] = val } confDir := filepath.Join(util.DataDir, box.ID, ".siyuan") if err = os.MkdirAll(confDir, 0755); nil != err { logging.LogErrorf("create conf dir failed: %s", err) return } confPath := filepath.Join(confDir, "sort.json") fullSortIDs := map[string]int{} var data []byte if filelock.IsExist(confPath) { data, err = filelock.ReadFile(confPath) if nil != err { logging.LogErrorf("read sort conf failed: %s", err) return } if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); nil != err { logging.LogErrorf("unmarshal sort conf failed: %s", err) } } for sortID, sortVal := range sortFolderIDs { fullSortIDs[sortID] = sortVal } data, err = gulu.JSON.MarshalJSON(fullSortIDs) if nil != err { logging.LogErrorf("marshal sort conf failed: %s", err) return } if err = filelock.WriteFile(confPath, data); nil != err { logging.LogErrorf("write sort conf failed: %s", err) return } IncSync() } func (box *Box) fillSort(files *[]*File) { confPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json") if !filelock.IsExist(confPath) { return } data, err := filelock.ReadFile(confPath) if nil != err { logging.LogErrorf("read sort conf failed: %s", err) return } fullSortIDs := map[string]int{} if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); nil != err { logging.LogErrorf("unmarshal sort conf failed: %s", err) return } for _, f := range *files { id := strings.TrimSuffix(f.ID, ".sy") f.Sort = fullSortIDs[id] } } func (box *Box) removeSort(ids []string) { confPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json") if !filelock.IsExist(confPath) { return } data, err := filelock.ReadFile(confPath) if nil != err { logging.LogErrorf("read sort conf failed: %s", err) return } fullSortIDs := map[string]int{} if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); nil != err { logging.LogErrorf("unmarshal sort conf failed: %s", err) return } for _, toRemove := range ids { delete(fullSortIDs, toRemove) } data, err = gulu.JSON.MarshalJSON(fullSortIDs) if nil != err { logging.LogErrorf("marshal sort conf failed: %s", err) return } if err = filelock.WriteFile(confPath, data); nil != err { logging.LogErrorf("write sort conf failed: %s", err) return } } func (box *Box) addMinSort(parentPath, id string) { docs, _, err := ListDocTree(box.ID, parentPath, util.SortModeUnassigned, false, false, 1) if nil != err { logging.LogErrorf("list doc tree failed: %s", err) return } sortVal := 0 if 0 < len(docs) { sortVal = docs[0].Sort - 1 } confDir := filepath.Join(util.DataDir, box.ID, ".siyuan") if err = os.MkdirAll(confDir, 0755); nil != err { logging.LogErrorf("create conf dir failed: %s", err) return } confPath := filepath.Join(confDir, "sort.json") fullSortIDs := map[string]int{} var data []byte if filelock.IsExist(confPath) { data, err = filelock.ReadFile(confPath) if nil != err { logging.LogErrorf("read sort conf failed: %s", err) return } if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); nil != err { logging.LogErrorf("unmarshal sort conf failed: %s", err) } } fullSortIDs[id] = sortVal data, err = gulu.JSON.MarshalJSON(fullSortIDs) if nil != err { logging.LogErrorf("marshal sort conf failed: %s", err) return } if err = filelock.WriteFile(confPath, data); nil != err { logging.LogErrorf("write sort conf failed: %s", err) return } }