2023-06-24 20:39:55 +08:00
// SiYuan - Refactor your thinking
2022-05-26 15:18:53 +08:00
// 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"
2025-07-17 16:33:22 +08:00
"time"
2022-05-26 15:18:53 +08:00
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
2023-02-05 18:01:33 +08:00
"github.com/siyuan-note/logging"
2022-05-26 15:18:53 +08:00
"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
2022-11-22 00:41:42 +08:00
tree , err := tx . loadTree ( headingID )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-05-26 15:18:53 +08:00
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 }
}
2023-10-24 00:56:10 +08:00
children := treenode . HeadingChildren ( heading )
2022-05-26 15:18:53 +08:00
for _ , child := range children {
childrenIDs = append ( childrenIDs , child . ID )
2023-10-24 00:56:10 +08:00
ast . Walk ( child , func ( n * ast . Node , entering bool ) ast . WalkStatus {
2023-10-26 11:40:52 +08:00
if ! entering || ! n . IsBlock ( ) {
return ast . WalkContinue
}
2024-09-08 21:59:13 +08:00
n . SetIALAttr ( "fold" , "1" )
2023-10-24 00:56:10 +08:00
n . SetIALAttr ( "heading-fold" , "1" )
return ast . WalkContinue
} )
2022-05-26 15:18:53 +08:00
}
heading . SetIALAttr ( "fold" , "1" )
2024-09-04 04:40:50 +03:00
if err = tx . writeTree ( tree ) ; err != nil {
2022-05-26 15:18:53 +08:00
return & TxErr { code : TxErrCodeWriteTree , msg : err . Error ( ) , id : headingID }
}
2022-07-14 21:50:46 +08:00
IncSync ( )
2022-05-26 15:18:53 +08:00
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
2022-11-22 00:41:42 +08:00
tree , err := tx . loadTree ( headingID )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-05-26 15:18:53 +08:00
return & TxErr { code : TxErrCodeBlockNotFound , id : headingID }
}
heading := treenode . GetNodeInTree ( tree , headingID )
if nil == heading {
return & TxErr { code : TxErrCodeBlockNotFound , id : headingID }
}
2023-10-24 00:56:10 +08:00
children := treenode . HeadingChildren ( heading )
2022-05-26 15:18:53 +08:00
for _ , child := range children {
2023-10-24 00:56:10 +08:00
ast . Walk ( child , func ( n * ast . Node , entering bool ) ast . WalkStatus {
2023-10-26 11:40:52 +08:00
if ! entering {
return ast . WalkContinue
}
2023-10-24 00:56:10 +08:00
n . RemoveIALAttr ( "heading-fold" )
n . RemoveIALAttr ( "fold" )
return ast . WalkContinue
} )
2022-05-26 15:18:53 +08:00
}
heading . RemoveIALAttr ( "fold" )
2022-06-19 17:01:21 +08:00
heading . RemoveIALAttr ( "heading-fold" )
2024-09-04 04:40:50 +03:00
if err = tx . writeTree ( tree ) ; err != nil {
2022-05-26 15:18:53 +08:00
return & TxErr { code : TxErrCodeWriteTree , msg : err . Error ( ) , id : headingID }
}
2022-07-14 21:50:46 +08:00
IncSync ( )
2022-05-26 15:18:53 +08:00
cache . PutBlockIAL ( headingID , parse . IAL2Map ( heading . KramdownIAL ) )
for _ , child := range children {
cache . PutBlockIAL ( child . ID , parse . IAL2Map ( child . KramdownIAL ) )
}
sql . UpsertTreeQueue ( tree )
2025-02-22 10:10:14 +08:00
// 展开折叠的标题后显示块引用计数 Display reference counts after unfolding headings https://github.com/siyuan-note/siyuan/issues/13618
fillBlockRefCount ( children )
2022-05-26 15:18:53 +08:00
luteEngine := NewLute ( )
operation . RetData = renderBlockDOMByNodes ( children , luteEngine )
return
}
func Doc2Heading ( srcID , targetID string , after bool ) ( srcTreeBox , srcTreePath string , err error ) {
2024-09-27 00:05:48 +08:00
if ! ast . IsNodeIDPattern ( srcID ) || ! ast . IsNodeIDPattern ( targetID ) {
return
}
2024-12-06 23:15:49 +08:00
FlushTxQueue ( )
2024-03-10 23:27:13 +08:00
srcTree , _ := LoadTreeByBlockID ( srcID )
2022-05-26 15:18:53 +08:00
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
}
2022-09-29 21:52:01 +08:00
2023-02-05 18:01:33 +08:00
if removeErr := os . Remove ( subDir ) ; nil != removeErr { // 移除空文件夹不会有副作用
logging . LogWarnf ( "remove empty dir [%s] failed: %s" , subDir , removeErr )
}
2022-05-26 15:18:53 +08:00
}
2024-09-27 00:05:48 +08:00
if nil == treenode . GetBlockTree ( targetID ) {
// 目标块不存在时忽略处理
return
}
2024-03-10 23:27:13 +08:00
targetTree , _ := LoadTreeByBlockID ( targetID )
2022-05-26 15:18:53 +08:00
if nil == targetTree {
2024-09-27 00:05:48 +08:00
// 目标块不存在时忽略处理
2022-05-26 15:18:53 +08:00
return
}
pivot := treenode . GetNodeInTree ( targetTree , targetID )
if nil == pivot {
err = ErrBlockNotFound
return
}
2025-03-15 11:05:33 +08:00
// 生成文档历史 https://github.com/siyuan-note/siyuan/issues/14359
2025-03-15 11:39:09 +08:00
generateOpTypeHistory ( srcTree , HistoryOpUpdate )
2025-03-15 11:05:33 +08:00
2023-03-29 19:38:03 +08:00
// 移动前先删除引用 https://github.com/siyuan-note/siyuan/issues/7819
sql . DeleteRefsTreeQueue ( srcTree )
sql . DeleteRefsTreeQueue ( targetTree )
2022-05-26 15:18:53 +08:00
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
}
2023-09-27 15:58:30 +08:00
srcTree . Root . RemoveIALAttr ( "scroll" ) // Remove `scroll` attribute when converting the document to a heading https://github.com/siyuan-note/siyuan/issues/9297
2022-05-26 15:18:53 +08:00
srcTree . Root . RemoveIALAttr ( "type" )
2022-11-11 21:24:55 +08:00
tagIAL := srcTree . Root . IALAttr ( "tags" )
tags := strings . Split ( tagIAL , "," )
srcTree . Root . RemoveIALAttr ( "tags" )
2022-05-26 15:18:53 +08:00
heading := & ast . Node { ID : srcTree . Root . ID , Type : ast . NodeHeading , HeadingLevel : headingLevel , KramdownIAL : srcTree . Root . KramdownIAL }
2023-02-05 18:01:33 +08:00
heading . SetIALAttr ( "updated" , util . CurrentTimeSecondsStr ( ) )
2022-05-26 15:18:53 +08:00
heading . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( srcTree . Root . IALAttr ( "title" ) ) } )
2025-06-12 21:45:42 +08:00
heading . RemoveIALAttr ( "title" )
2023-02-05 18:01:33 +08:00
heading . Box , heading . Path = targetTree . Box , targetTree . Path
2022-11-11 21:27:35 +08:00
if "" != tagIAL && 0 < len ( tags ) {
2022-11-11 21:24:55 +08:00
// 带标签的文档块转换为标题块时将标签移动到标题块下方 https://github.com/siyuan-note/siyuan/issues/6550
2024-11-10 12:04:56 +08:00
tagPara := treenode . NewParagraph ( "" )
2022-11-11 21:24:55 +08:00
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 ( " " ) } )
}
}
2022-11-11 21:27:35 +08:00
if nil != tagPara . FirstChild {
srcTree . Root . PrependChild ( tagPara )
}
2022-11-11 21:24:55 +08:00
}
2022-05-26 15:18:53 +08:00
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 )
}
2023-02-22 08:59:25 +08:00
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 } )
2023-02-22 08:31:46 +08:00
evt := util . NewCmdResult ( "removeDoc" , 0 , util . PushModeBroadcast )
evt . Data = map [ string ] interface { } {
2023-02-22 08:59:25 +08:00
"ids" : [ ] string { srcTree . ID } ,
2023-02-22 08:31:46 +08:00
}
util . PushEvent ( evt )
2023-02-05 18:01:33 +08:00
srcTreeBox , srcTreePath = srcTree . Box , srcTree . Path // 返回旧的文档块位置,前端后续会删除旧的文档块
2022-05-26 15:18:53 +08:00
targetTree . Root . SetIALAttr ( "updated" , util . CurrentTimeSecondsStr ( ) )
2023-02-22 09:05:41 +08:00
treenode . RemoveBlockTreesByRootID ( srcTree . ID )
treenode . RemoveBlockTreesByRootID ( targetTree . ID )
2024-04-11 21:54:34 +08:00
err = indexWriteTreeUpsertQueue ( targetTree )
2022-07-14 21:50:46 +08:00
IncSync ( )
2025-02-19 12:11:03 +08:00
go func ( ) {
2025-07-17 16:33:22 +08:00
time . Sleep ( util . SQLFlushInterval )
RefreshBacklink ( srcTree . ID )
RefreshBacklink ( targetTree . ID )
2025-02-19 12:11:03 +08:00
ResetVirtualBlockRefCache ( )
} ( )
2022-05-26 15:18:53 +08:00
return
}
2024-11-22 22:36:19 +08:00
func Heading2Doc ( srcHeadingID , targetBoxID , targetPath , previousPath string ) ( srcRootBlockID , newTargetPath string , err error ) {
2024-12-06 23:15:49 +08:00
FlushTxQueue ( )
2024-03-10 23:27:13 +08:00
srcTree , _ := LoadTreeByBlockID ( srcHeadingID )
2022-05-26 15:18:53 +08:00
if nil == srcTree {
err = ErrBlockNotFound
return
}
srcRootBlockID = srcTree . Root . ID
2023-01-27 17:57:32 +08:00
headingBlock , err := getBlock ( srcHeadingID , srcTree )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-05-26 15:18:53 +08:00
return
}
if nil == headingBlock {
err = ErrBlockNotFound
return
}
headingNode := treenode . GetNodeInTree ( srcTree , srcHeadingID )
if nil == headingNode {
err = ErrBlockNotFound
return
}
box := Conf . Box ( targetBoxID )
2025-01-02 11:10:16 +08:00
headingText := getNodeRefText0 ( headingNode , Conf . Editor . BlockRefDynamicAnchorTextMaxLen , true )
2024-04-24 21:18:15 +08:00
if strings . Contains ( headingText , "/" ) {
headingText = strings . ReplaceAll ( headingText , "/" , "_" )
util . PushMsg ( Conf . language ( 246 ) , 7000 )
}
2022-05-26 15:18:53 +08:00
moveToRoot := "/" == targetPath
toHP := path . Join ( "/" , headingText )
toFolder := "/"
2024-11-22 22:36:19 +08:00
if "" != previousPath {
previousDoc := treenode . GetBlockTreeRootByPath ( targetBoxID , previousPath )
if nil == previousDoc {
2022-05-26 15:18:53 +08:00
err = ErrBlockNotFound
return
}
2024-11-22 22:36:19 +08:00
parentPath := path . Dir ( previousPath )
if "/" != parentPath {
parentPath = strings . TrimSuffix ( parentPath , "/" ) + ".sy"
parentDoc := treenode . GetBlockTreeRootByPath ( targetBoxID , parentPath )
if nil == parentDoc {
err = ErrBlockNotFound
return
}
toHP = path . Join ( parentDoc . HPath , headingText )
toFolder = path . Join ( path . Dir ( parentPath ) , parentDoc . ID )
}
} else {
if ! moveToRoot {
parentDoc := treenode . GetBlockTreeRootByPath ( targetBoxID , targetPath )
if nil == parentDoc {
err = ErrBlockNotFound
return
}
toHP = path . Join ( parentDoc . HPath , headingText )
toFolder = path . Join ( path . Dir ( targetPath ) , parentDoc . ID )
}
2022-05-26 15:18:53 +08:00
}
newTargetPath = path . Join ( toFolder , srcHeadingID + ".sy" )
if ! box . Exist ( toFolder ) {
2024-09-04 04:40:50 +03:00
if err = box . MkdirAll ( toFolder ) ; err != nil {
2022-05-26 15:18:53 +08:00
return
}
}
// 折叠标题转换为文档时需要自动展开下方块 https://github.com/siyuan-note/siyuan/issues/2947
2022-06-19 17:01:21 +08:00
children := treenode . HeadingChildren ( headingNode )
2022-05-26 15:18:53 +08:00
for _ , child := range children {
2023-10-26 11:40:52 +08:00
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
} )
2022-05-26 15:18:53 +08:00
}
headingNode . RemoveIALAttr ( "fold" )
2023-10-26 11:40:52 +08:00
headingNode . RemoveIALAttr ( "heading-fold" )
2022-05-26 15:18:53 +08:00
2023-02-10 14:28:10 +08:00
luteEngine := util . NewLute ( )
2022-05-26 15:18:53 +08:00
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 {
2024-04-15 09:21:15 +08:00
c . HeadingLevel = c . HeadingLevel - topLevel + 2
2022-05-26 15:18:53 +08:00
if 6 < c . HeadingLevel {
c . HeadingLevel = 6
}
}
}
headingNode . Unlink ( )
srcTree . Root . SetIALAttr ( "updated" , util . CurrentTimeSecondsStr ( ) )
2022-08-05 22:37:16 +08:00
if nil == srcTree . Root . FirstChild {
2024-11-10 12:04:56 +08:00
srcTree . Root . AppendChild ( treenode . NewParagraph ( "" ) )
2022-08-05 22:37:16 +08:00
}
2023-02-22 09:05:41 +08:00
treenode . RemoveBlockTreesByRootID ( srcTree . ID )
2024-09-04 04:40:50 +03:00
if err = indexWriteTreeUpsertQueue ( srcTree ) ; err != nil {
2022-05-26 15:18:53 +08:00
return "" , "" , err
}
newTree . Box , newTree . Path = targetBoxID , newTargetPath
newTree . Root . SetIALAttr ( "updated" , util . CurrentTimeSecondsStr ( ) )
2022-11-11 21:24:55 +08:00
newTree . Root . Spec = "1"
2024-11-22 22:36:19 +08:00
if "" != previousPath {
2024-11-29 09:07:17 +08:00
box . addSort ( previousPath , newTree . ID )
2024-11-22 22:36:19 +08:00
} else {
box . addMinSort ( path . Dir ( newTargetPath ) , newTree . ID )
}
2024-09-04 04:40:50 +03:00
if err = indexWriteTreeUpsertQueue ( newTree ) ; err != nil {
2022-05-26 15:18:53 +08:00
return "" , "" , err
}
2022-07-14 21:50:46 +08:00
IncSync ( )
2025-02-19 12:11:03 +08:00
go func ( ) {
2025-07-17 16:33:22 +08:00
RefreshBacklink ( srcTree . ID )
RefreshBacklink ( newTree . ID )
2025-02-19 12:11:03 +08:00
ResetVirtualBlockRefCache ( )
} ( )
2022-05-26 15:18:53 +08:00
return
}