A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 446 lines 13 kB view raw
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 "os" 22 "path" 23 "path/filepath" 24 "strings" 25 "time" 26 27 "github.com/88250/gulu" 28 "github.com/88250/lute/ast" 29 "github.com/88250/lute/parse" 30 "github.com/siyuan-note/logging" 31 "github.com/siyuan-note/siyuan/kernel/cache" 32 "github.com/siyuan-note/siyuan/kernel/sql" 33 "github.com/siyuan-note/siyuan/kernel/treenode" 34 "github.com/siyuan-note/siyuan/kernel/util" 35) 36 37func (tx *Transaction) doFoldHeading(operation *Operation) (ret *TxErr) { 38 headingID := operation.ID 39 tree, err := tx.loadTree(headingID) 40 if err != nil { 41 return &TxErr{code: TxErrCodeBlockNotFound, id: headingID} 42 } 43 44 childrenIDs := []string{} // 这里不能用 nil,否则折叠下方没内容的标题时会内核中断 https://github.com/siyuan-note/siyuan/issues/3643 45 heading := treenode.GetNodeInTree(tree, headingID) 46 if nil == heading { 47 return &TxErr{code: TxErrCodeBlockNotFound, id: headingID} 48 } 49 50 children := treenode.HeadingChildren(heading) 51 for _, child := range children { 52 childrenIDs = append(childrenIDs, child.ID) 53 ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus { 54 if !entering || !n.IsBlock() { 55 return ast.WalkContinue 56 } 57 58 n.SetIALAttr("fold", "1") 59 n.SetIALAttr("heading-fold", "1") 60 return ast.WalkContinue 61 }) 62 } 63 heading.SetIALAttr("fold", "1") 64 if err = tx.writeTree(tree); err != nil { 65 return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: headingID} 66 } 67 IncSync() 68 69 cache.PutBlockIAL(headingID, parse.IAL2Map(heading.KramdownIAL)) 70 for _, child := range children { 71 cache.PutBlockIAL(child.ID, parse.IAL2Map(child.KramdownIAL)) 72 } 73 sql.UpsertTreeQueue(tree) 74 operation.RetData = childrenIDs 75 return 76} 77 78func (tx *Transaction) doUnfoldHeading(operation *Operation) (ret *TxErr) { 79 headingID := operation.ID 80 81 tree, err := tx.loadTree(headingID) 82 if err != nil { 83 return &TxErr{code: TxErrCodeBlockNotFound, id: headingID} 84 } 85 86 heading := treenode.GetNodeInTree(tree, headingID) 87 if nil == heading { 88 return &TxErr{code: TxErrCodeBlockNotFound, id: headingID} 89 } 90 91 luteEngine := NewLute() 92 parentFoldedHeading := treenode.GetParentFoldedHeading(heading) 93 if nil != parentFoldedHeading { 94 // 如果当前标题在上方某个折叠的标题下方,则展开上方那个折叠标题以保持一致性 95 children := treenode.HeadingChildren(parentFoldedHeading) 96 for _, child := range children { 97 ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus { 98 if !entering || !n.IsBlock() { 99 return ast.WalkContinue 100 } 101 102 n.RemoveIALAttr("heading-fold") 103 n.RemoveIALAttr("fold") 104 return ast.WalkContinue 105 }) 106 } 107 parentFoldedHeading.RemoveIALAttr("fold") 108 parentFoldedHeading.RemoveIALAttr("heading-fold") 109 go func() { 110 tx.WaitForCommit() 111 ReloadProtyle(tree.ID) 112 }() 113 } 114 115 children := treenode.HeadingChildren(heading) 116 for _, child := range children { 117 ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus { 118 if !entering { 119 return ast.WalkContinue 120 } 121 122 n.RemoveIALAttr("heading-fold") 123 n.RemoveIALAttr("fold") 124 return ast.WalkContinue 125 }) 126 } 127 heading.RemoveIALAttr("fold") 128 heading.RemoveIALAttr("heading-fold") 129 if err = tx.writeTree(tree); err != nil { 130 return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: headingID} 131 } 132 IncSync() 133 134 cache.PutBlockIAL(headingID, parse.IAL2Map(heading.KramdownIAL)) 135 for _, child := range children { 136 cache.PutBlockIAL(child.ID, parse.IAL2Map(child.KramdownIAL)) 137 } 138 sql.UpsertTreeQueue(tree) 139 140 // 展开折叠的标题后显示块引用计数 Display reference counts after unfolding headings https://github.com/siyuan-note/siyuan/issues/13618 141 fillBlockRefCount(children) 142 143 operation.RetData = renderBlockDOMByNodes(children, luteEngine) 144 return 145} 146 147func Doc2Heading(srcID, targetID string, after bool) (srcTreeBox, srcTreePath string, err error) { 148 if !ast.IsNodeIDPattern(srcID) || !ast.IsNodeIDPattern(targetID) { 149 return 150 } 151 152 FlushTxQueue() 153 154 srcTree, _ := LoadTreeByBlockID(srcID) 155 if nil == srcTree { 156 err = ErrBlockNotFound 157 return 158 } 159 160 subDir := filepath.Join(util.DataDir, srcTree.Box, strings.TrimSuffix(srcTree.Path, ".sy")) 161 if gulu.File.IsDir(subDir) { 162 if !util.IsEmptyDir(subDir) { 163 err = errors.New(Conf.Language(20)) 164 return 165 } 166 167 if removeErr := os.Remove(subDir); nil != removeErr { // 移除空文件夹不会有副作用 168 logging.LogWarnf("remove empty dir [%s] failed: %s", subDir, removeErr) 169 } 170 } 171 172 if nil == treenode.GetBlockTree(targetID) { 173 // 目标块不存在时忽略处理 174 return 175 } 176 177 targetTree, _ := LoadTreeByBlockID(targetID) 178 if nil == targetTree { 179 // 目标块不存在时忽略处理 180 return 181 } 182 183 pivot := treenode.GetNodeInTree(targetTree, targetID) 184 if nil == pivot { 185 err = ErrBlockNotFound 186 return 187 } 188 189 // 生成文档历史 https://github.com/siyuan-note/siyuan/issues/14359 190 generateOpTypeHistory(srcTree, HistoryOpUpdate) 191 192 // 移动前先删除引用 https://github.com/siyuan-note/siyuan/issues/7819 193 sql.DeleteRefsTreeQueue(srcTree) 194 sql.DeleteRefsTreeQueue(targetTree) 195 196 if ast.NodeListItem == pivot.Type { 197 pivot = pivot.LastChild 198 } 199 200 pivotLevel := treenode.HeadingLevel(pivot) 201 deltaLevel := pivotLevel - treenode.TopHeadingLevel(srcTree) + 1 202 headingLevel := pivotLevel 203 if ast.NodeHeading == pivot.Type { // 平级插入 204 children := treenode.HeadingChildren(pivot) 205 if after { 206 if length := len(children); 0 < length { 207 pivot = children[length-1] 208 } 209 } 210 } else { // 子节点插入 211 headingLevel++ 212 deltaLevel++ 213 } 214 if 6 < headingLevel { 215 headingLevel = 6 216 } 217 218 srcTree.Root.RemoveIALAttr("scroll") // Remove `scroll` attribute when converting the document to a heading https://github.com/siyuan-note/siyuan/issues/9297 219 srcTree.Root.RemoveIALAttr("type") 220 tagIAL := srcTree.Root.IALAttr("tags") 221 tags := strings.Split(tagIAL, ",") 222 srcTree.Root.RemoveIALAttr("tags") 223 heading := &ast.Node{ID: srcTree.Root.ID, Type: ast.NodeHeading, HeadingLevel: headingLevel, KramdownIAL: srcTree.Root.KramdownIAL} 224 heading.SetIALAttr("updated", util.CurrentTimeSecondsStr()) 225 heading.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(srcTree.Root.IALAttr("title"))}) 226 heading.RemoveIALAttr("title") 227 heading.Box, heading.Path = targetTree.Box, targetTree.Path 228 if "" != tagIAL && 0 < len(tags) { 229 // 带标签的文档块转换为标题块时将标签移动到标题块下方 https://github.com/siyuan-note/siyuan/issues/6550 230 231 tagPara := treenode.NewParagraph("") 232 for i, tag := range tags { 233 if "" == tag { 234 continue 235 } 236 237 tagPara.AppendChild(&ast.Node{Type: ast.NodeTextMark, TextMarkType: "tag", TextMarkTextContent: tag}) 238 if i < len(tags)-1 { 239 tagPara.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(" ")}) 240 } 241 } 242 if nil != tagPara.FirstChild { 243 srcTree.Root.PrependChild(tagPara) 244 } 245 } 246 247 var nodes []*ast.Node 248 if after { 249 for c := srcTree.Root.LastChild; nil != c; c = c.Previous { 250 nodes = append(nodes, c) 251 } 252 } else { 253 for c := srcTree.Root.FirstChild; nil != c; c = c.Next { 254 nodes = append(nodes, c) 255 } 256 } 257 258 if !after { 259 pivot.InsertBefore(heading) 260 } 261 262 for _, n := range nodes { 263 if ast.NodeHeading == n.Type { 264 n.HeadingLevel = n.HeadingLevel + deltaLevel 265 if 6 < n.HeadingLevel { 266 n.HeadingLevel = 6 267 } 268 } 269 n.Box = targetTree.Box 270 n.Path = targetTree.Path 271 if after { 272 pivot.InsertAfter(n) 273 } else { 274 pivot.InsertBefore(n) 275 } 276 } 277 278 if after { 279 pivot.InsertAfter(heading) 280 } 281 282 box := Conf.Box(srcTree.Box) 283 if removeErr := box.Remove(srcTree.Path); nil != removeErr { 284 logging.LogWarnf("remove tree [%s] failed: %s", srcTree.Path, removeErr) 285 } 286 box.removeSort([]string{srcTree.ID}) 287 RemoveRecentDoc([]string{srcTree.ID}) 288 evt := util.NewCmdResult("removeDoc", 0, util.PushModeBroadcast) 289 evt.Data = map[string]interface{}{ 290 "ids": []string{srcTree.ID}, 291 } 292 util.PushEvent(evt) 293 294 srcTreeBox, srcTreePath = srcTree.Box, srcTree.Path // 返回旧的文档块位置,前端后续会删除旧的文档块 295 targetTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr()) 296 treenode.RemoveBlockTreesByRootID(srcTree.ID) 297 treenode.RemoveBlockTreesByRootID(targetTree.ID) 298 err = indexWriteTreeUpsertQueue(targetTree) 299 IncSync() 300 go func() { 301 time.Sleep(util.SQLFlushInterval) 302 RefreshBacklink(srcTree.ID) 303 RefreshBacklink(targetTree.ID) 304 ResetVirtualBlockRefCache() 305 }() 306 return 307} 308 309func Heading2Doc(srcHeadingID, targetBoxID, targetPath, previousPath string) (srcRootBlockID, newTargetPath string, err error) { 310 FlushTxQueue() 311 312 srcTree, _ := LoadTreeByBlockID(srcHeadingID) 313 if nil == srcTree { 314 err = ErrBlockNotFound 315 return 316 } 317 srcRootBlockID = srcTree.Root.ID 318 319 headingBlock, err := getBlock(srcHeadingID, srcTree) 320 if err != nil { 321 return 322 } 323 if nil == headingBlock { 324 err = ErrBlockNotFound 325 return 326 } 327 headingNode := treenode.GetNodeInTree(srcTree, srcHeadingID) 328 if nil == headingNode { 329 err = ErrBlockNotFound 330 return 331 } 332 333 box := Conf.Box(targetBoxID) 334 headingText := getNodeRefText0(headingNode, Conf.Editor.BlockRefDynamicAnchorTextMaxLen, true) 335 if strings.Contains(headingText, "/") { 336 headingText = strings.ReplaceAll(headingText, "/", "_") 337 util.PushMsg(Conf.language(246), 7000) 338 } 339 340 moveToRoot := "/" == targetPath 341 toHP := path.Join("/", headingText) 342 toFolder := "/" 343 if "" != previousPath { 344 previousDoc := treenode.GetBlockTreeRootByPath(targetBoxID, previousPath) 345 if nil == previousDoc { 346 err = ErrBlockNotFound 347 return 348 } 349 parentPath := path.Dir(previousPath) 350 if "/" != parentPath { 351 parentPath = strings.TrimSuffix(parentPath, "/") + ".sy" 352 parentDoc := treenode.GetBlockTreeRootByPath(targetBoxID, parentPath) 353 if nil == parentDoc { 354 err = ErrBlockNotFound 355 return 356 } 357 toHP = path.Join(parentDoc.HPath, headingText) 358 toFolder = path.Join(path.Dir(parentPath), parentDoc.ID) 359 } 360 } else { 361 if !moveToRoot { 362 parentDoc := treenode.GetBlockTreeRootByPath(targetBoxID, targetPath) 363 if nil == parentDoc { 364 err = ErrBlockNotFound 365 return 366 } 367 toHP = path.Join(parentDoc.HPath, headingText) 368 toFolder = path.Join(path.Dir(targetPath), parentDoc.ID) 369 } 370 } 371 372 newTargetPath = path.Join(toFolder, srcHeadingID+".sy") 373 if !box.Exist(toFolder) { 374 if err = box.MkdirAll(toFolder); err != nil { 375 return 376 } 377 } 378 379 // 折叠标题转换为文档时需要自动展开下方块 https://github.com/siyuan-note/siyuan/issues/2947 380 children := treenode.HeadingChildren(headingNode) 381 for _, child := range children { 382 ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus { 383 if !entering { 384 return ast.WalkContinue 385 } 386 387 n.RemoveIALAttr("heading-fold") 388 n.RemoveIALAttr("fold") 389 return ast.WalkContinue 390 }) 391 } 392 headingNode.RemoveIALAttr("fold") 393 headingNode.RemoveIALAttr("heading-fold") 394 395 luteEngine := util.NewLute() 396 newTree := &parse.Tree{Root: &ast.Node{Type: ast.NodeDocument, ID: srcHeadingID}, Context: &parse.Context{ParseOption: luteEngine.ParseOptions}} 397 for _, c := range children { 398 newTree.Root.AppendChild(c) 399 } 400 newTree.ID = srcHeadingID 401 newTree.Path = newTargetPath 402 newTree.HPath = toHP 403 headingNode.SetIALAttr("type", "doc") 404 headingNode.SetIALAttr("id", srcHeadingID) 405 headingNode.SetIALAttr("title", headingText) 406 newTree.Root.KramdownIAL = headingNode.KramdownIAL 407 408 topLevel := treenode.TopHeadingLevel(newTree) 409 for c := newTree.Root.FirstChild; nil != c; c = c.Next { 410 if ast.NodeHeading == c.Type { 411 c.HeadingLevel = c.HeadingLevel - topLevel + 2 412 if 6 < c.HeadingLevel { 413 c.HeadingLevel = 6 414 } 415 } 416 } 417 418 headingNode.Unlink() 419 srcTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr()) 420 if nil == srcTree.Root.FirstChild { 421 srcTree.Root.AppendChild(treenode.NewParagraph("")) 422 } 423 treenode.RemoveBlockTreesByRootID(srcTree.ID) 424 if err = indexWriteTreeUpsertQueue(srcTree); err != nil { 425 return "", "", err 426 } 427 428 newTree.Box, newTree.Path = targetBoxID, newTargetPath 429 newTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr()) 430 newTree.Root.Spec = "1" 431 if "" != previousPath { 432 box.addSort(previousPath, newTree.ID) 433 } else { 434 box.addMinSort(path.Dir(newTargetPath), newTree.ID) 435 } 436 if err = indexWriteTreeUpsertQueue(newTree); err != nil { 437 return "", "", err 438 } 439 IncSync() 440 go func() { 441 RefreshBacklink(srcTree.ID) 442 RefreshBacklink(newTree.ID) 443 ResetVirtualBlockRefCache() 444 }() 445 return 446}