siyuan/kernel/model/heading.go

386 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 <https://www.gnu.org/licenses/>.
package model
import (
"errors"
"os"
"path"
"path/filepath"
"strings"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/cache"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func (tx *Transaction) doFoldHeading(operation *Operation) (ret *TxErr) {
headingID := operation.ID
tree, err := tx.loadTree(headingID)
if err != nil {
return &TxErr{code: TxErrCodeBlockNotFound, id: headingID}
}
childrenIDs := []string{} // 这里不能用 nil否则折叠下方没内容的标题时会内核中断 https://github.com/siyuan-note/siyuan/issues/3643
heading := treenode.GetNodeInTree(tree, headingID)
if nil == heading {
return &TxErr{code: TxErrCodeBlockNotFound, id: headingID}
}
children := treenode.HeadingChildren(heading)
for _, child := range children {
childrenIDs = append(childrenIDs, child.ID)
ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || !n.IsBlock() {
return ast.WalkContinue
}
n.SetIALAttr("fold", "1")
n.SetIALAttr("heading-fold", "1")
return ast.WalkContinue
})
}
heading.SetIALAttr("fold", "1")
if err = tx.writeTree(tree); err != nil {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: headingID}
}
IncSync()
cache.PutBlockIAL(headingID, parse.IAL2Map(heading.KramdownIAL))
for _, child := range children {
cache.PutBlockIAL(child.ID, parse.IAL2Map(child.KramdownIAL))
}
sql.UpsertTreeQueue(tree)
operation.RetData = childrenIDs
return
}
func (tx *Transaction) doUnfoldHeading(operation *Operation) (ret *TxErr) {
headingID := operation.ID
tree, err := tx.loadTree(headingID)
if err != nil {
return &TxErr{code: TxErrCodeBlockNotFound, id: headingID}
}
heading := treenode.GetNodeInTree(tree, headingID)
if nil == heading {
return &TxErr{code: TxErrCodeBlockNotFound, id: headingID}
}
children := treenode.HeadingChildren(heading)
for _, child := range children {
ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
n.RemoveIALAttr("heading-fold")
n.RemoveIALAttr("fold")
return ast.WalkContinue
})
}
heading.RemoveIALAttr("fold")
heading.RemoveIALAttr("heading-fold")
if err = tx.writeTree(tree); err != nil {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: headingID}
}
IncSync()
cache.PutBlockIAL(headingID, parse.IAL2Map(heading.KramdownIAL))
for _, child := range children {
cache.PutBlockIAL(child.ID, parse.IAL2Map(child.KramdownIAL))
}
sql.UpsertTreeQueue(tree)
luteEngine := NewLute()
operation.RetData = renderBlockDOMByNodes(children, luteEngine)
return
}
func Doc2Heading(srcID, targetID string, after bool) (srcTreeBox, srcTreePath string, err error) {
if !ast.IsNodeIDPattern(srcID) || !ast.IsNodeIDPattern(targetID) {
return
}
srcTree, _ := LoadTreeByBlockID(srcID)
if nil == srcTree {
err = ErrBlockNotFound
return
}
subDir := filepath.Join(util.DataDir, srcTree.Box, strings.TrimSuffix(srcTree.Path, ".sy"))
if gulu.File.IsDir(subDir) {
if !util.IsEmptyDir(subDir) {
err = errors.New(Conf.Language(20))
return
}
if removeErr := os.Remove(subDir); nil != removeErr { // 移除空文件夹不会有副作用
logging.LogWarnf("remove empty dir [%s] failed: %s", subDir, removeErr)
}
}
if nil == treenode.GetBlockTree(targetID) {
// 目标块不存在时忽略处理
return
}
targetTree, _ := LoadTreeByBlockID(targetID)
if nil == targetTree {
// 目标块不存在时忽略处理
return
}
pivot := treenode.GetNodeInTree(targetTree, targetID)
if nil == pivot {
err = ErrBlockNotFound
return
}
// 移动前先删除引用 https://github.com/siyuan-note/siyuan/issues/7819
sql.DeleteRefsTreeQueue(srcTree)
sql.DeleteRefsTreeQueue(targetTree)
if ast.NodeListItem == pivot.Type {
pivot = pivot.LastChild
}
pivotLevel := treenode.HeadingLevel(pivot)
deltaLevel := pivotLevel - treenode.TopHeadingLevel(srcTree) + 1
headingLevel := pivotLevel
if ast.NodeHeading == pivot.Type { // 平级插入
children := treenode.HeadingChildren(pivot)
if after {
if length := len(children); 0 < length {
pivot = children[length-1]
}
}
} else { // 子节点插入
headingLevel++
deltaLevel++
}
if 6 < headingLevel {
headingLevel = 6
}
srcTree.Root.RemoveIALAttr("scroll") // Remove `scroll` attribute when converting the document to a heading https://github.com/siyuan-note/siyuan/issues/9297
srcTree.Root.RemoveIALAttr("type")
tagIAL := srcTree.Root.IALAttr("tags")
tags := strings.Split(tagIAL, ",")
srcTree.Root.RemoveIALAttr("tags")
heading := &ast.Node{ID: srcTree.Root.ID, Type: ast.NodeHeading, HeadingLevel: headingLevel, KramdownIAL: srcTree.Root.KramdownIAL}
heading.SetIALAttr("updated", util.CurrentTimeSecondsStr())
heading.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(srcTree.Root.IALAttr("title"))})
heading.Box, heading.Path = targetTree.Box, targetTree.Path
if "" != tagIAL && 0 < len(tags) {
// 带标签的文档块转换为标题块时将标签移动到标题块下方 https://github.com/siyuan-note/siyuan/issues/6550
tagPara := treenode.NewParagraph("")
for i, tag := range tags {
if "" == tag {
continue
}
tagPara.AppendChild(&ast.Node{Type: ast.NodeTextMark, TextMarkType: "tag", TextMarkTextContent: tag})
if i < len(tags)-1 {
tagPara.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(" ")})
}
}
if nil != tagPara.FirstChild {
srcTree.Root.PrependChild(tagPara)
}
}
var nodes []*ast.Node
if after {
for c := srcTree.Root.LastChild; nil != c; c = c.Previous {
nodes = append(nodes, c)
}
} else {
for c := srcTree.Root.FirstChild; nil != c; c = c.Next {
nodes = append(nodes, c)
}
}
if !after {
pivot.InsertBefore(heading)
}
for _, n := range nodes {
if ast.NodeHeading == n.Type {
n.HeadingLevel = n.HeadingLevel + deltaLevel
if 6 < n.HeadingLevel {
n.HeadingLevel = 6
}
}
n.Box = targetTree.Box
n.Path = targetTree.Path
if after {
pivot.InsertAfter(n)
} else {
pivot.InsertBefore(n)
}
}
if after {
pivot.InsertAfter(heading)
}
if contentPivot := treenode.GetNodeInTree(targetTree, targetID); nil != contentPivot && ast.NodeParagraph == contentPivot.Type && nil == contentPivot.FirstChild { // 插入到空的段落块下
contentPivot.Unlink()
}
box := Conf.Box(srcTree.Box)
if removeErr := box.Remove(srcTree.Path); nil != removeErr {
logging.LogWarnf("remove tree [%s] failed: %s", srcTree.Path, removeErr)
}
box.removeSort([]string{srcTree.ID})
RemoveRecentDoc([]string{srcTree.ID})
evt := util.NewCmdResult("removeDoc", 0, util.PushModeBroadcast)
evt.Data = map[string]interface{}{
"ids": []string{srcTree.ID},
}
util.PushEvent(evt)
srcTreeBox, srcTreePath = srcTree.Box, srcTree.Path // 返回旧的文档块位置,前端后续会删除旧的文档块
targetTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
treenode.RemoveBlockTreesByRootID(srcTree.ID)
treenode.RemoveBlockTreesByRootID(targetTree.ID)
err = indexWriteTreeUpsertQueue(targetTree)
IncSync()
RefreshBacklink(srcTree.ID)
RefreshBacklink(targetTree.ID)
return
}
func Heading2Doc(srcHeadingID, targetBoxID, targetPath string) (srcRootBlockID, newTargetPath string, err error) {
srcTree, _ := LoadTreeByBlockID(srcHeadingID)
if nil == srcTree {
err = ErrBlockNotFound
return
}
srcRootBlockID = srcTree.Root.ID
headingBlock, err := getBlock(srcHeadingID, srcTree)
if err != nil {
return
}
if nil == headingBlock {
err = ErrBlockNotFound
return
}
headingNode := treenode.GetNodeInTree(srcTree, srcHeadingID)
if nil == headingNode {
err = ErrBlockNotFound
return
}
box := Conf.Box(targetBoxID)
headingText := getNodeRefText0(headingNode, Conf.Editor.BlockRefDynamicAnchorTextMaxLen)
if strings.Contains(headingText, "/") {
headingText = strings.ReplaceAll(headingText, "/", "_")
util.PushMsg(Conf.language(246), 7000)
}
moveToRoot := "/" == targetPath
toHP := path.Join("/", headingText)
toFolder := "/"
if !moveToRoot {
toBlock := treenode.GetBlockTreeRootByPath(targetBoxID, targetPath)
if nil == toBlock {
err = ErrBlockNotFound
return
}
toHP = path.Join(toBlock.HPath, headingText)
toFolder = path.Join(path.Dir(targetPath), toBlock.ID)
}
newTargetPath = path.Join(toFolder, srcHeadingID+".sy")
if !box.Exist(toFolder) {
if err = box.MkdirAll(toFolder); err != nil {
return
}
}
// 折叠标题转换为文档时需要自动展开下方块 https://github.com/siyuan-note/siyuan/issues/2947
children := treenode.HeadingChildren(headingNode)
for _, child := range children {
ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
n.RemoveIALAttr("heading-fold")
n.RemoveIALAttr("fold")
return ast.WalkContinue
})
}
headingNode.RemoveIALAttr("fold")
headingNode.RemoveIALAttr("heading-fold")
luteEngine := util.NewLute()
newTree := &parse.Tree{Root: &ast.Node{Type: ast.NodeDocument, ID: srcHeadingID}, Context: &parse.Context{ParseOption: luteEngine.ParseOptions}}
for _, c := range children {
newTree.Root.AppendChild(c)
}
newTree.ID = srcHeadingID
newTree.Path = newTargetPath
newTree.HPath = toHP
headingNode.SetIALAttr("type", "doc")
headingNode.SetIALAttr("id", srcHeadingID)
headingNode.SetIALAttr("title", headingText)
newTree.Root.KramdownIAL = headingNode.KramdownIAL
topLevel := treenode.TopHeadingLevel(newTree)
for c := newTree.Root.FirstChild; nil != c; c = c.Next {
if ast.NodeHeading == c.Type {
c.HeadingLevel = c.HeadingLevel - topLevel + 2
if 6 < c.HeadingLevel {
c.HeadingLevel = 6
}
}
}
headingNode.Unlink()
srcTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
if nil == srcTree.Root.FirstChild {
srcTree.Root.AppendChild(treenode.NewParagraph(""))
}
treenode.RemoveBlockTreesByRootID(srcTree.ID)
if err = indexWriteTreeUpsertQueue(srcTree); err != nil {
return "", "", err
}
newTree.Box, newTree.Path = targetBoxID, newTargetPath
newTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
newTree.Root.Spec = "1"
box.addMinSort(path.Dir(newTargetPath), newTree.ID)
if err = indexWriteTreeUpsertQueue(newTree); err != nil {
return "", "", err
}
IncSync()
RefreshBacklink(srcTree.ID)
RefreshBacklink(newTree.ID)
return
}