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 (
|
|
|
|
|
"bytes"
|
2023-09-14 09:12:41 +08:00
|
|
|
|
"errors"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
"path/filepath"
|
2024-09-06 17:04:07 +08:00
|
|
|
|
"slices"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
2023-12-08 20:28:57 +08:00
|
|
|
|
"sync/atomic"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/88250/gulu"
|
2023-02-10 14:28:10 +08:00
|
|
|
|
"github.com/88250/lute"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"github.com/88250/lute/ast"
|
2022-09-14 00:35:13 +08:00
|
|
|
|
"github.com/88250/lute/editor"
|
2022-11-12 00:16:24 +08:00
|
|
|
|
"github.com/88250/lute/lex"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"github.com/88250/lute/parse"
|
2022-06-15 23:56:47 +08:00
|
|
|
|
"github.com/siyuan-note/filelock"
|
2022-07-17 12:22:32 +08:00
|
|
|
|
"github.com/siyuan-note/logging"
|
2023-07-11 22:11:15 +08:00
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/av"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/cache"
|
2023-02-10 14:28:10 +08:00
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/filesys"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/sql"
|
2024-09-06 00:07:57 +08:00
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/task"
|
2022-05-26 15:18:53 +08:00
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/treenode"
|
|
|
|
|
"github.com/siyuan-note/siyuan/kernel/util"
|
|
|
|
|
)
|
|
|
|
|
|
2024-03-29 23:22:29 +08:00
|
|
|
|
func IsMoveOutlineHeading(transactions *[]*Transaction) bool {
|
|
|
|
|
for _, tx := range *transactions {
|
|
|
|
|
for _, op := range tx.DoOperations {
|
|
|
|
|
if "moveOutlineHeading" == op.Action {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-22 19:20:44 +08:00
|
|
|
|
func FlushTxQueue() {
|
2024-10-25 20:35:29 +08:00
|
|
|
|
time.Sleep(time.Duration(50) * time.Millisecond)
|
2024-10-22 19:20:44 +08:00
|
|
|
|
for 0 < len(txQueue) || isFlushing {
|
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-04 11:45:41 +08:00
|
|
|
|
var (
|
2024-07-04 11:17:31 +08:00
|
|
|
|
txQueue = make(chan *Transaction, 7)
|
|
|
|
|
flushLock = sync.Mutex{}
|
|
|
|
|
isFlushing = false
|
2023-10-04 11:45:41 +08:00
|
|
|
|
)
|
|
|
|
|
|
2024-10-22 19:20:44 +08:00
|
|
|
|
func init() {
|
|
|
|
|
go flushQueue()
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-10-22 19:20:44 +08:00
|
|
|
|
func flushQueue() {
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case tx := <-txQueue:
|
|
|
|
|
flushTx(tx)
|
2023-10-04 11:45:41 +08:00
|
|
|
|
}
|
2024-10-22 19:20:44 +08:00
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-10-04 11:45:41 +08:00
|
|
|
|
func flushTx(tx *Transaction) {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
defer logging.Recover()
|
2023-01-27 16:59:39 +08:00
|
|
|
|
flushLock.Lock()
|
2024-07-04 11:17:31 +08:00
|
|
|
|
isFlushing = true
|
|
|
|
|
defer func() {
|
|
|
|
|
isFlushing = false
|
|
|
|
|
flushLock.Unlock()
|
|
|
|
|
}()
|
2022-05-26 15:18:53 +08:00
|
|
|
|
|
|
|
|
|
start := time.Now()
|
2023-10-04 11:45:41 +08:00
|
|
|
|
if txErr := performTx(tx); nil != txErr {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
switch txErr.code {
|
|
|
|
|
case TxErrCodeBlockNotFound:
|
|
|
|
|
util.PushTxErr("Transaction failed", txErr.code, nil)
|
|
|
|
|
return
|
2023-04-09 23:02:09 +08:00
|
|
|
|
case TxErrCodeDataIsSyncing:
|
2024-01-13 22:14:26 +08:00
|
|
|
|
util.PushMsg(Conf.Language(222), 5000)
|
2025-07-05 12:33:41 +08:00
|
|
|
|
case TxErrHandleAttributeView:
|
2025-06-23 09:48:09 +08:00
|
|
|
|
util.PushMsg(Conf.language(258), 5000)
|
2025-07-05 12:33:11 +08:00
|
|
|
|
logging.LogErrorf("handle attribute view failed: %s", txErr.msg)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
default:
|
2023-12-25 15:23:01 +08:00
|
|
|
|
txData, _ := gulu.JSON.MarshalJSON(tx)
|
|
|
|
|
logging.LogFatalf(logging.ExitCodeFatal, "transaction failed [%d]: %s\n tx [%s]", txErr.code, txErr.msg, txData)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
elapsed := time.Now().Sub(start).Milliseconds()
|
2023-10-04 11:45:41 +08:00
|
|
|
|
if 0 < len(tx.DoOperations) {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if 2000 < elapsed {
|
2023-01-26 23:30:29 +08:00
|
|
|
|
logging.LogWarnf("op tx [%dms]", elapsed)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-19 00:12:28 +08:00
|
|
|
|
func PerformTransactions(transactions *[]*Transaction) {
|
2023-10-04 11:45:41 +08:00
|
|
|
|
for _, tx := range *transactions {
|
2023-12-08 20:28:57 +08:00
|
|
|
|
tx.m = &sync.Mutex{}
|
2023-10-04 11:45:41 +08:00
|
|
|
|
txQueue <- tx
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const (
|
2025-07-05 12:33:41 +08:00
|
|
|
|
TxErrCodeBlockNotFound = 0
|
|
|
|
|
TxErrCodeDataIsSyncing = 1
|
|
|
|
|
TxErrCodeWriteTree = 2
|
|
|
|
|
TxErrHandleAttributeView = 3
|
2022-05-26 15:18:53 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type TxErr struct {
|
|
|
|
|
code int
|
|
|
|
|
msg string
|
|
|
|
|
id string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func performTx(tx *Transaction) (ret *TxErr) {
|
|
|
|
|
if 1 > len(tx.DoOperations) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//os.MkdirAll("pprof", 0755)
|
|
|
|
|
//cpuProfile, _ := os.Create("pprof/cpu_profile_tx")
|
|
|
|
|
//pprof.StartCPUProfile(cpuProfile)
|
|
|
|
|
//defer pprof.StopCPUProfile()
|
|
|
|
|
|
|
|
|
|
var err error
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = tx.begin(); err != nil {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if strings.Contains(err.Error(), "database is closed") {
|
|
|
|
|
return
|
|
|
|
|
}
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("begin tx failed: %s", err)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
ret = &TxErr{msg: err.Error()}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-24 15:23:30 +08:00
|
|
|
|
defer func() {
|
|
|
|
|
if e := recover(); nil != e {
|
2025-06-14 10:16:46 +08:00
|
|
|
|
msg := fmt.Sprintf("PANIC RECOVERED: %v\n\t%s\n", e, logging.ShortStack())
|
2023-12-24 15:23:30 +08:00
|
|
|
|
logging.LogErrorf(msg)
|
|
|
|
|
|
2024-02-07 23:37:06 +08:00
|
|
|
|
if 1 == tx.state.Load() {
|
2023-12-24 15:23:30 +08:00
|
|
|
|
tx.rollback()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
for _, op := range tx.DoOperations {
|
|
|
|
|
switch op.Action {
|
|
|
|
|
case "create":
|
|
|
|
|
ret = tx.doCreate(op)
|
|
|
|
|
case "update":
|
|
|
|
|
ret = tx.doUpdate(op)
|
|
|
|
|
case "insert":
|
|
|
|
|
ret = tx.doInsert(op)
|
|
|
|
|
case "delete":
|
|
|
|
|
ret = tx.doDelete(op)
|
|
|
|
|
case "move":
|
|
|
|
|
ret = tx.doMove(op)
|
2024-03-27 22:15:52 +08:00
|
|
|
|
case "moveOutlineHeading":
|
|
|
|
|
ret = tx.doMoveOutlineHeading(op)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
case "append":
|
|
|
|
|
ret = tx.doAppend(op)
|
|
|
|
|
case "appendInsert":
|
|
|
|
|
ret = tx.doAppendInsert(op)
|
|
|
|
|
case "prependInsert":
|
|
|
|
|
ret = tx.doPrependInsert(op)
|
|
|
|
|
case "foldHeading":
|
|
|
|
|
ret = tx.doFoldHeading(op)
|
|
|
|
|
case "unfoldHeading":
|
|
|
|
|
ret = tx.doUnfoldHeading(op)
|
2022-11-12 00:16:24 +08:00
|
|
|
|
case "setAttrs":
|
2023-04-09 23:00:09 +08:00
|
|
|
|
ret = tx.doSetAttrs(op)
|
2023-10-13 22:50:11 +08:00
|
|
|
|
case "doUpdateUpdated":
|
|
|
|
|
ret = tx.doUpdateUpdated(op)
|
2023-04-09 23:00:09 +08:00
|
|
|
|
case "addFlashcards":
|
|
|
|
|
ret = tx.doAddFlashcards(op)
|
|
|
|
|
case "removeFlashcards":
|
|
|
|
|
ret = tx.doRemoveFlashcards(op)
|
2023-07-11 22:11:15 +08:00
|
|
|
|
case "setAttrViewName":
|
|
|
|
|
ret = tx.doSetAttrViewName(op)
|
|
|
|
|
case "setAttrViewFilters":
|
|
|
|
|
ret = tx.doSetAttrViewFilters(op)
|
|
|
|
|
case "setAttrViewSorts":
|
|
|
|
|
ret = tx.doSetAttrViewSorts(op)
|
2023-12-08 21:05:21 +08:00
|
|
|
|
case "setAttrViewPageSize":
|
|
|
|
|
ret = tx.doSetAttrViewPageSize(op)
|
2023-07-11 22:44:31 +08:00
|
|
|
|
case "setAttrViewColWidth":
|
|
|
|
|
ret = tx.doSetAttrViewColumnWidth(op)
|
|
|
|
|
case "setAttrViewColWrap":
|
|
|
|
|
ret = tx.doSetAttrViewColumnWrap(op)
|
|
|
|
|
case "setAttrViewColHidden":
|
|
|
|
|
ret = tx.doSetAttrViewColumnHidden(op)
|
2023-11-10 10:22:19 +08:00
|
|
|
|
case "setAttrViewColPin":
|
|
|
|
|
ret = tx.doSetAttrViewColumnPin(op)
|
2023-10-05 11:12:57 +08:00
|
|
|
|
case "setAttrViewColIcon":
|
|
|
|
|
ret = tx.doSetAttrViewColumnIcon(op)
|
2024-11-09 14:09:40 +08:00
|
|
|
|
case "setAttrViewColDesc":
|
|
|
|
|
ret = tx.doSetAttrViewColumnDesc(op)
|
2023-03-02 18:17:06 +08:00
|
|
|
|
case "insertAttrViewBlock":
|
2023-03-02 18:42:19 +08:00
|
|
|
|
ret = tx.doInsertAttrViewBlock(op)
|
2023-03-02 18:17:06 +08:00
|
|
|
|
case "removeAttrViewBlock":
|
2023-03-02 18:42:19 +08:00
|
|
|
|
ret = tx.doRemoveAttrViewBlock(op)
|
2023-03-15 20:31:41 +08:00
|
|
|
|
case "addAttrViewCol":
|
|
|
|
|
ret = tx.doAddAttrViewColumn(op)
|
2023-07-01 10:23:58 +08:00
|
|
|
|
case "updateAttrViewCol":
|
|
|
|
|
ret = tx.doUpdateAttrViewColumn(op)
|
2023-07-12 00:02:40 +08:00
|
|
|
|
case "removeAttrViewCol":
|
|
|
|
|
ret = tx.doRemoveAttrViewColumn(op)
|
2023-07-11 23:56:23 +08:00
|
|
|
|
case "sortAttrViewRow":
|
|
|
|
|
ret = tx.doSortAttrViewRow(op)
|
|
|
|
|
case "sortAttrViewCol":
|
|
|
|
|
ret = tx.doSortAttrViewColumn(op)
|
2024-05-16 22:24:15 +08:00
|
|
|
|
case "sortAttrViewKey":
|
|
|
|
|
ret = tx.doSortAttrViewKey(op)
|
2023-06-10 15:00:04 +08:00
|
|
|
|
case "updateAttrViewCell":
|
|
|
|
|
ret = tx.doUpdateAttrViewCell(op)
|
2023-07-13 00:08:10 +08:00
|
|
|
|
case "updateAttrViewColOptions":
|
|
|
|
|
ret = tx.doUpdateAttrViewColOptions(op)
|
2023-07-13 00:16:40 +08:00
|
|
|
|
case "removeAttrViewColOption":
|
|
|
|
|
ret = tx.doRemoveAttrViewColOption(op)
|
|
|
|
|
case "updateAttrViewColOption":
|
|
|
|
|
ret = tx.doUpdateAttrViewColOption(op)
|
2024-11-09 14:09:40 +08:00
|
|
|
|
case "setAttrViewColOptionDesc":
|
|
|
|
|
ret = tx.doSetAttrViewColOptionDesc(op)
|
2023-07-15 21:36:35 +08:00
|
|
|
|
case "setAttrViewColCalc":
|
|
|
|
|
ret = tx.doSetAttrViewColCalc(op)
|
2023-08-03 23:38:45 +08:00
|
|
|
|
case "updateAttrViewColNumberFormat":
|
|
|
|
|
ret = tx.doUpdateAttrViewColNumberFormat(op)
|
2023-09-28 10:50:35 +08:00
|
|
|
|
case "replaceAttrViewBlock":
|
|
|
|
|
ret = tx.doReplaceAttrViewBlock(op)
|
2023-10-01 17:33:53 +08:00
|
|
|
|
case "updateAttrViewColTemplate":
|
|
|
|
|
ret = tx.doUpdateAttrViewColTemplate(op)
|
2023-11-30 19:48:30 +08:00
|
|
|
|
case "addAttrViewView":
|
|
|
|
|
ret = tx.doAddAttrViewView(op)
|
|
|
|
|
case "removeAttrViewView":
|
|
|
|
|
ret = tx.doRemoveAttrViewView(op)
|
2023-12-01 09:00:14 +08:00
|
|
|
|
case "setAttrViewViewName":
|
|
|
|
|
ret = tx.doSetAttrViewViewName(op)
|
2023-12-01 09:48:20 +08:00
|
|
|
|
case "setAttrViewViewIcon":
|
|
|
|
|
ret = tx.doSetAttrViewViewIcon(op)
|
2024-11-09 14:09:40 +08:00
|
|
|
|
case "setAttrViewViewDesc":
|
|
|
|
|
ret = tx.doSetAttrViewViewDesc(op)
|
2023-12-01 09:48:20 +08:00
|
|
|
|
case "duplicateAttrViewView":
|
|
|
|
|
ret = tx.doDuplicateAttrViewView(op)
|
2023-12-01 15:39:21 +08:00
|
|
|
|
case "sortAttrViewView":
|
|
|
|
|
ret = tx.doSortAttrViewView(op)
|
2023-12-23 11:35:53 +08:00
|
|
|
|
case "updateAttrViewColRelation":
|
|
|
|
|
ret = tx.doUpdateAttrViewColRelation(op)
|
2023-12-24 21:47:10 +08:00
|
|
|
|
case "updateAttrViewColRollup":
|
|
|
|
|
ret = tx.doUpdateAttrViewColRollup(op)
|
2024-03-01 20:38:53 +08:00
|
|
|
|
case "hideAttrViewName":
|
|
|
|
|
ret = tx.doHideAttrViewName(op)
|
2024-04-04 11:33:51 +08:00
|
|
|
|
case "setAttrViewColDate":
|
|
|
|
|
ret = tx.doSetAttrViewColDate(op)
|
2024-04-15 17:47:03 +08:00
|
|
|
|
case "unbindAttrViewBlock":
|
|
|
|
|
ret = tx.doUnbindAttrViewBlock(op)
|
2024-06-10 16:25:57 +08:00
|
|
|
|
case "duplicateAttrViewKey":
|
|
|
|
|
ret = tx.doDuplicateAttrViewKey(op)
|
2025-06-09 22:26:08 +08:00
|
|
|
|
case "setAttrViewCoverFrom":
|
|
|
|
|
ret = tx.doSetAttrViewCoverFrom(op)
|
2025-06-09 22:27:51 +08:00
|
|
|
|
case "setAttrViewCoverFromAssetKeyID":
|
|
|
|
|
ret = tx.doSetAttrViewCoverFromAssetKeyID(op)
|
2025-06-09 22:30:00 +08:00
|
|
|
|
case "setAttrViewCardSize":
|
|
|
|
|
ret = tx.doSetAttrViewCardSize(op)
|
2025-06-09 22:31:29 +08:00
|
|
|
|
case "setAttrViewFitImage":
|
|
|
|
|
ret = tx.doSetAttrViewFitImage(op)
|
2025-06-09 22:33:25 +08:00
|
|
|
|
case "setAttrViewShowIcon":
|
|
|
|
|
ret = tx.doSetAttrViewShowIcon(op)
|
2025-06-09 22:34:07 +08:00
|
|
|
|
case "setAttrViewWrapField":
|
|
|
|
|
ret = tx.doSetAttrViewWrapField(op)
|
2025-06-10 12:23:07 +08:00
|
|
|
|
case "changeAttrViewLayout":
|
|
|
|
|
ret = tx.doChangeAttrViewLayout(op)
|
2025-06-16 10:40:56 +08:00
|
|
|
|
case "setAttrViewBlockView":
|
|
|
|
|
ret = tx.doSetAttrViewBlockView(op)
|
2025-06-17 17:46:32 +08:00
|
|
|
|
case "setAttrViewCardAspectRatio":
|
|
|
|
|
ret = tx.doSetAttrViewCardAspectRatio(op)
|
2025-07-04 16:48:09 +08:00
|
|
|
|
case "setAttrViewGroup":
|
2025-07-01 17:56:32 +08:00
|
|
|
|
ret = tx.doSetAttrViewGroup(op)
|
2025-07-06 17:41:43 +08:00
|
|
|
|
case "hideAttrViewGroup":
|
|
|
|
|
ret = tx.doHideAttrViewGroup(op)
|
2025-07-11 10:43:09 +08:00
|
|
|
|
case "syncAttrViewTableColWidth":
|
|
|
|
|
ret = tx.doSyncAttrViewTableColWidth(op)
|
2025-07-25 12:05:33 +08:00
|
|
|
|
case "removeAttrViewGroup":
|
|
|
|
|
ret = tx.doRemoveAttrViewGroup(op)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if nil != ret {
|
|
|
|
|
tx.rollback()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if cr := tx.commit(); nil != cr {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("commit tx failed: %s", cr)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{msg: cr.Error()}
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (tx *Transaction) doMove(operation *Operation) (ret *TxErr) {
|
|
|
|
|
var err error
|
|
|
|
|
id := operation.ID
|
|
|
|
|
srcTree, err := tx.loadTree(id)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-01-28 10:24:51 +08:00
|
|
|
|
logging.LogErrorf("load tree [%s] failed: %s", id, err)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
srcNode := treenode.GetNodeInTree(srcTree, id)
|
|
|
|
|
if nil == srcNode {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("get node [%s] in tree [%s] failed", id, srcTree.Root.ID)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-15 11:39:09 +08:00
|
|
|
|
// 生成文档历史 https://github.com/siyuan-note/siyuan/issues/14359
|
|
|
|
|
generateOpTypeHistory(srcTree, HistoryOpUpdate)
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
var headingChildren []*ast.Node
|
|
|
|
|
if isMovingFoldHeading := ast.NodeHeading == srcNode.Type && "1" == srcNode.IALAttr("fold"); isMovingFoldHeading {
|
2022-06-19 17:01:21 +08:00
|
|
|
|
headingChildren = treenode.HeadingChildren(srcNode)
|
2023-05-22 11:06:28 +08:00
|
|
|
|
// Blocks below other non-folded headings are no longer moved when moving a folded heading https://github.com/siyuan-note/siyuan/issues/8321
|
|
|
|
|
headingChildren = treenode.GetHeadingFold(headingChildren)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
2023-05-22 11:06:28 +08:00
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
var srcEmptyList *ast.Node
|
|
|
|
|
if ast.NodeListItem == srcNode.Type && srcNode.Parent.FirstChild == srcNode && srcNode.Parent.LastChild == srcNode {
|
|
|
|
|
// 列表中唯一的列表项被移除后,该列表就为空了
|
|
|
|
|
srcEmptyList = srcNode.Parent
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetPreviousID := operation.PreviousID
|
|
|
|
|
targetParentID := operation.ParentID
|
|
|
|
|
if "" != targetPreviousID {
|
|
|
|
|
if id == targetPreviousID {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var targetTree *parse.Tree
|
|
|
|
|
targetTree, err = tx.loadTree(targetPreviousID)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-01-28 10:24:51 +08:00
|
|
|
|
logging.LogErrorf("load tree [%s] failed: %s", targetPreviousID, err)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: targetPreviousID}
|
|
|
|
|
}
|
|
|
|
|
isSameTree := srcTree.ID == targetTree.ID
|
|
|
|
|
if isSameTree {
|
|
|
|
|
targetTree = srcTree
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetNode := treenode.GetNodeInTree(targetTree, targetPreviousID)
|
|
|
|
|
if nil == targetNode {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("get node [%s] in tree [%s] failed", targetPreviousID, targetTree.Root.ID)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: targetPreviousID}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ast.NodeHeading == targetNode.Type && "1" == targetNode.IALAttr("fold") {
|
2022-06-19 17:01:21 +08:00
|
|
|
|
targetChildren := treenode.HeadingChildren(targetNode)
|
2023-05-22 11:06:28 +08:00
|
|
|
|
targetChildren = treenode.GetHeadingFold(targetChildren)
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if l := len(targetChildren); 0 < l {
|
|
|
|
|
targetNode = targetChildren[l-1]
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-26 01:09:36 +08:00
|
|
|
|
|
2023-01-26 01:16:01 +08:00
|
|
|
|
if isMovingFoldHeadingIntoSelf(targetNode, headingChildren) {
|
|
|
|
|
return
|
2023-01-26 01:09:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-11-16 17:29:59 +08:00
|
|
|
|
if isMovingParentIntoChild(srcNode, targetNode) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
for i := len(headingChildren) - 1; -1 < i; i-- {
|
|
|
|
|
c := headingChildren[i]
|
|
|
|
|
targetNode.InsertAfter(c)
|
|
|
|
|
}
|
|
|
|
|
targetNode.InsertAfter(srcNode)
|
|
|
|
|
if nil != srcEmptyList {
|
|
|
|
|
srcEmptyList.Unlink()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refreshUpdated(srcNode)
|
2024-09-06 01:03:21 +08:00
|
|
|
|
tx.nodes[srcNode.ID] = srcNode
|
2022-05-26 15:18:53 +08:00
|
|
|
|
refreshUpdated(srcTree.Root)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = tx.writeTree(srcTree); err != nil {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !isSameTree {
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = tx.writeTree(targetTree); err != nil {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
2025-07-17 16:33:22 +08:00
|
|
|
|
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, srcTree.ID)
|
|
|
|
|
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, srcNode.ID)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if id == targetParentID {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetTree, err := tx.loadTree(targetParentID)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-01-28 10:24:51 +08:00
|
|
|
|
logging.LogErrorf("load tree [%s] failed: %s", targetParentID, err)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: targetParentID}
|
|
|
|
|
}
|
|
|
|
|
isSameTree := srcTree.ID == targetTree.ID
|
|
|
|
|
if isSameTree {
|
|
|
|
|
targetTree = srcTree
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetNode := treenode.GetNodeInTree(targetTree, targetParentID)
|
|
|
|
|
if nil == targetNode {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("get node [%s] in tree [%s] failed", targetParentID, targetTree.Root.ID)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: targetParentID}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-26 01:16:01 +08:00
|
|
|
|
if isMovingFoldHeadingIntoSelf(targetNode, headingChildren) {
|
|
|
|
|
return
|
2023-01-26 01:09:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-11-16 17:29:59 +08:00
|
|
|
|
if isMovingParentIntoChild(srcNode, targetNode) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
processed := false
|
|
|
|
|
if ast.NodeSuperBlock == targetNode.Type {
|
|
|
|
|
// 在布局节点后插入
|
|
|
|
|
targetNode = targetNode.FirstChild.Next
|
|
|
|
|
for i := len(headingChildren) - 1; -1 < i; i-- {
|
|
|
|
|
c := headingChildren[i]
|
|
|
|
|
targetNode.InsertAfter(c)
|
|
|
|
|
}
|
|
|
|
|
targetNode.InsertAfter(srcNode)
|
|
|
|
|
if nil != srcEmptyList {
|
|
|
|
|
srcEmptyList.Unlink()
|
|
|
|
|
}
|
|
|
|
|
processed = true
|
|
|
|
|
} else if ast.NodeListItem == targetNode.Type {
|
|
|
|
|
if 3 == targetNode.ListData.Typ {
|
|
|
|
|
// 在任务列表标记节点后插入
|
|
|
|
|
targetNode = targetNode.FirstChild
|
|
|
|
|
for i := len(headingChildren) - 1; -1 < i; i-- {
|
|
|
|
|
c := headingChildren[i]
|
|
|
|
|
targetNode.InsertAfter(c)
|
|
|
|
|
}
|
|
|
|
|
targetNode.InsertAfter(srcNode)
|
|
|
|
|
if nil != srcEmptyList {
|
|
|
|
|
srcEmptyList.Unlink()
|
|
|
|
|
}
|
|
|
|
|
processed = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !processed {
|
|
|
|
|
for i := len(headingChildren) - 1; -1 < i; i-- {
|
|
|
|
|
c := headingChildren[i]
|
|
|
|
|
targetNode.PrependChild(c)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetNode.PrependChild(srcNode)
|
|
|
|
|
if nil != srcEmptyList {
|
|
|
|
|
srcEmptyList.Unlink()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refreshUpdated(srcNode)
|
2024-09-06 01:03:21 +08:00
|
|
|
|
tx.nodes[srcNode.ID] = srcNode
|
2022-05-26 15:18:53 +08:00
|
|
|
|
refreshUpdated(srcTree.Root)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = tx.writeTree(srcTree); err != nil {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
|
|
|
|
|
}
|
|
|
|
|
if !isSameTree {
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = tx.writeTree(targetTree); err != nil {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
|
|
|
|
|
}
|
2025-07-17 16:33:22 +08:00
|
|
|
|
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, srcTree.ID)
|
|
|
|
|
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, srcNode.ID)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-26 01:16:01 +08:00
|
|
|
|
func isMovingFoldHeadingIntoSelf(targetNode *ast.Node, headingChildren []*ast.Node) bool {
|
|
|
|
|
for _, headingChild := range headingChildren {
|
|
|
|
|
if headingChild.ID == targetNode.ID {
|
|
|
|
|
// 不能将折叠标题移动到自己下方节点的前或后 https://github.com/siyuan-note/siyuan/issues/7163
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-16 17:29:59 +08:00
|
|
|
|
func isMovingParentIntoChild(srcNode, targetNode *ast.Node) bool {
|
|
|
|
|
for parent := targetNode.Parent; nil != parent; parent = parent.Parent {
|
|
|
|
|
if parent.ID == srcNode.ID {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
func (tx *Transaction) doPrependInsert(operation *Operation) (ret *TxErr) {
|
|
|
|
|
var err error
|
|
|
|
|
block := treenode.GetBlockTree(operation.ParentID)
|
|
|
|
|
if nil == block {
|
2023-09-14 09:42:25 +08:00
|
|
|
|
logging.LogWarnf("not found block [%s]", operation.ParentID)
|
2023-09-14 09:31:23 +08:00
|
|
|
|
util.ReloadUI() // 比如分屏后编辑器状态不一致,这里强制重新载入界面
|
|
|
|
|
return
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
tree, err := tx.loadTree(block.ID)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-01-28 10:24:51 +08:00
|
|
|
|
msg := fmt.Sprintf("load tree [%s] failed: %s", block.ID, err)
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf(msg)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: block.ID}
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-14 00:35:13 +08:00
|
|
|
|
data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
|
2023-02-10 14:28:10 +08:00
|
|
|
|
subTree := tx.luteEngine.BlockDOM2Tree(data)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
insertedNode := subTree.Root.FirstChild
|
|
|
|
|
if nil == insertedNode {
|
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: block.ID}
|
|
|
|
|
}
|
|
|
|
|
var remains []*ast.Node
|
|
|
|
|
for remain := insertedNode.Next; nil != remain; remain = remain.Next {
|
|
|
|
|
if ast.NodeKramdownBlockIAL != remain.Type {
|
|
|
|
|
if "" == remain.ID {
|
|
|
|
|
remain.ID = ast.NewNodeID()
|
|
|
|
|
remain.SetIALAttr("id", remain.ID)
|
|
|
|
|
}
|
|
|
|
|
remains = append(remains, remain)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if "" == insertedNode.ID {
|
|
|
|
|
insertedNode.ID = ast.NewNodeID()
|
|
|
|
|
insertedNode.SetIALAttr("id", insertedNode.ID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
node := treenode.GetNodeInTree(tree, operation.ParentID)
|
|
|
|
|
if nil == node {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("get node [%s] in tree [%s] failed", operation.ParentID, tree.Root.ID)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
|
|
|
|
|
}
|
|
|
|
|
isContainer := node.IsContainerBlock()
|
|
|
|
|
for i := len(remains) - 1; 0 <= i; i-- {
|
|
|
|
|
remain := remains[i]
|
|
|
|
|
if isContainer {
|
|
|
|
|
if ast.NodeListItem == node.Type && 3 == node.ListData.Typ {
|
|
|
|
|
node.FirstChild.InsertAfter(remain)
|
|
|
|
|
} else if ast.NodeSuperBlock == node.Type {
|
|
|
|
|
node.FirstChild.Next.InsertAfter(remain)
|
|
|
|
|
} else {
|
|
|
|
|
node.PrependChild(remain)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
node.InsertAfter(remain)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if isContainer {
|
|
|
|
|
if ast.NodeListItem == node.Type && 3 == node.ListData.Typ {
|
|
|
|
|
node.FirstChild.InsertAfter(insertedNode)
|
|
|
|
|
} else if ast.NodeSuperBlock == node.Type {
|
|
|
|
|
node.FirstChild.Next.InsertAfter(insertedNode)
|
|
|
|
|
} else {
|
|
|
|
|
node.PrependChild(insertedNode)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
node.InsertAfter(insertedNode)
|
|
|
|
|
}
|
|
|
|
|
createdUpdated(insertedNode)
|
|
|
|
|
tx.nodes[insertedNode.ID] = insertedNode
|
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: block.ID}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
operation.ID = insertedNode.ID
|
|
|
|
|
operation.ParentID = insertedNode.Parent.ID
|
|
|
|
|
|
|
|
|
|
// 将 prependInsert 转换为 insert 推送
|
|
|
|
|
operation.Action = "insert"
|
|
|
|
|
if nil != insertedNode.Previous {
|
|
|
|
|
operation.PreviousID = insertedNode.Previous.ID
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (tx *Transaction) doAppendInsert(operation *Operation) (ret *TxErr) {
|
|
|
|
|
var err error
|
|
|
|
|
block := treenode.GetBlockTree(operation.ParentID)
|
|
|
|
|
if nil == block {
|
2023-09-14 09:42:25 +08:00
|
|
|
|
logging.LogWarnf("not found block [%s]", operation.ParentID)
|
2023-09-14 09:31:23 +08:00
|
|
|
|
util.ReloadUI() // 比如分屏后编辑器状态不一致,这里强制重新载入界面
|
|
|
|
|
return
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
tree, err := tx.loadTree(block.ID)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-01-28 10:24:51 +08:00
|
|
|
|
msg := fmt.Sprintf("load tree [%s] failed: %s", block.ID, err)
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf(msg)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: block.ID}
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-14 00:35:13 +08:00
|
|
|
|
data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
|
2023-02-10 14:28:10 +08:00
|
|
|
|
subTree := tx.luteEngine.BlockDOM2Tree(data)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
insertedNode := subTree.Root.FirstChild
|
|
|
|
|
if nil == insertedNode {
|
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: block.ID}
|
|
|
|
|
}
|
|
|
|
|
if "" == insertedNode.ID {
|
|
|
|
|
insertedNode.ID = ast.NewNodeID()
|
|
|
|
|
insertedNode.SetIALAttr("id", insertedNode.ID)
|
|
|
|
|
}
|
|
|
|
|
var toInserts []*ast.Node
|
|
|
|
|
for toInsert := insertedNode; nil != toInsert; toInsert = toInsert.Next {
|
|
|
|
|
if ast.NodeKramdownBlockIAL != toInsert.Type {
|
|
|
|
|
if "" == toInsert.ID {
|
|
|
|
|
toInsert.ID = ast.NewNodeID()
|
|
|
|
|
toInsert.SetIALAttr("id", toInsert.ID)
|
|
|
|
|
}
|
|
|
|
|
toInserts = append(toInserts, toInsert)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
node := treenode.GetNodeInTree(tree, operation.ParentID)
|
|
|
|
|
if nil == node {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("get node [%s] in tree [%s] failed", operation.ParentID, tree.Root.ID)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
|
|
|
|
|
}
|
|
|
|
|
isContainer := node.IsContainerBlock()
|
|
|
|
|
for i := 0; i < len(toInserts); i++ {
|
|
|
|
|
toInsert := toInserts[i]
|
|
|
|
|
if isContainer {
|
2023-12-22 20:53:59 +08:00
|
|
|
|
if ast.NodeList == node.Type {
|
|
|
|
|
// 列表下只能挂列表项,所以这里需要分情况处理 https://github.com/siyuan-note/siyuan/issues/9955
|
|
|
|
|
if ast.NodeList == toInsert.Type {
|
|
|
|
|
var childLis []*ast.Node
|
|
|
|
|
for childLi := toInsert.FirstChild; nil != childLi; childLi = childLi.Next {
|
|
|
|
|
childLis = append(childLis, childLi)
|
|
|
|
|
}
|
|
|
|
|
for _, childLi := range childLis {
|
|
|
|
|
node.AppendChild(childLi)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
newLiID := ast.NewNodeID()
|
|
|
|
|
newLi := &ast.Node{ID: newLiID, Type: ast.NodeListItem, ListData: &ast.ListData{Typ: node.ListData.Typ}}
|
|
|
|
|
newLi.SetIALAttr("id", newLiID)
|
|
|
|
|
node.AppendChild(newLi)
|
|
|
|
|
newLi.AppendChild(toInsert)
|
|
|
|
|
}
|
|
|
|
|
} else if ast.NodeSuperBlock == node.Type {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
node.LastChild.InsertBefore(toInsert)
|
|
|
|
|
} else {
|
|
|
|
|
node.AppendChild(toInsert)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
node.InsertAfter(toInsert)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createdUpdated(insertedNode)
|
|
|
|
|
tx.nodes[insertedNode.ID] = insertedNode
|
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: block.ID}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
operation.ID = insertedNode.ID
|
|
|
|
|
operation.ParentID = insertedNode.Parent.ID
|
|
|
|
|
|
|
|
|
|
// 将 appendInsert 转换为 insert 推送
|
|
|
|
|
operation.Action = "insert"
|
|
|
|
|
if nil != insertedNode.Previous {
|
|
|
|
|
operation.PreviousID = insertedNode.Previous.ID
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (tx *Transaction) doAppend(operation *Operation) (ret *TxErr) {
|
|
|
|
|
var err error
|
|
|
|
|
id := operation.ID
|
|
|
|
|
srcTree, err := tx.loadTree(id)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-01-28 10:24:51 +08:00
|
|
|
|
logging.LogErrorf("load tree [%s] failed: %s", id, err)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
srcNode := treenode.GetNodeInTree(srcTree, id)
|
|
|
|
|
if nil == srcNode {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("get node [%s] in tree [%s] failed", id, srcTree.Root.ID)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ast.NodeDocument == srcNode.Type {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogWarnf("can't append a root to another root")
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var headingChildren []*ast.Node
|
|
|
|
|
if isMovingFoldHeading := ast.NodeHeading == srcNode.Type && "1" == srcNode.IALAttr("fold"); isMovingFoldHeading {
|
2022-06-19 17:01:21 +08:00
|
|
|
|
headingChildren = treenode.HeadingChildren(srcNode)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
var srcEmptyList, targetNewList *ast.Node
|
|
|
|
|
if ast.NodeListItem == srcNode.Type {
|
|
|
|
|
targetNewListID := ast.NewNodeID()
|
|
|
|
|
targetNewList = &ast.Node{ID: targetNewListID, Type: ast.NodeList, ListData: &ast.ListData{Typ: srcNode.ListData.Typ}}
|
|
|
|
|
targetNewList.SetIALAttr("id", targetNewListID)
|
|
|
|
|
if srcNode.Parent.FirstChild == srcNode && srcNode.Parent.LastChild == srcNode {
|
|
|
|
|
// 列表中唯一的列表项被移除后,该列表就为空了
|
|
|
|
|
srcEmptyList = srcNode.Parent
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetRootID := operation.ParentID
|
|
|
|
|
if id == targetRootID {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogWarnf("target root id is nil")
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetTree, err := tx.loadTree(targetRootID)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-01-28 10:24:51 +08:00
|
|
|
|
logging.LogErrorf("load tree [%s] failed: %s", targetRootID, err)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: targetRootID}
|
|
|
|
|
}
|
|
|
|
|
isSameTree := srcTree.ID == targetTree.ID
|
|
|
|
|
if isSameTree {
|
|
|
|
|
targetTree = srcTree
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetRoot := targetTree.Root
|
|
|
|
|
if nil != targetNewList {
|
|
|
|
|
if nil != targetRoot.LastChild {
|
|
|
|
|
if ast.NodeList != targetRoot.LastChild.Type {
|
|
|
|
|
targetNewList.AppendChild(srcNode)
|
|
|
|
|
targetRoot.AppendChild(targetNewList)
|
|
|
|
|
} else {
|
|
|
|
|
targetRoot.LastChild.AppendChild(srcNode)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
targetRoot.AppendChild(srcNode)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
targetRoot.AppendChild(srcNode)
|
|
|
|
|
}
|
|
|
|
|
for _, c := range headingChildren {
|
|
|
|
|
targetRoot.AppendChild(c)
|
|
|
|
|
}
|
|
|
|
|
if nil != srcEmptyList {
|
|
|
|
|
srcEmptyList.Unlink()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = tx.writeTree(srcTree); err != nil {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !isSameTree {
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = tx.writeTree(targetTree); err != nil {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (tx *Transaction) doDelete(operation *Operation) (ret *TxErr) {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
// logging.LogInfof("commit delete [%+v]", operation)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
id := operation.ID
|
|
|
|
|
tree, err := tx.loadTree(id)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-09-14 09:12:41 +08:00
|
|
|
|
if errors.Is(err, ErrBlockNotFound) {
|
|
|
|
|
// move 以后这里会空,算作正常情况
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-28 10:24:51 +08:00
|
|
|
|
msg := fmt.Sprintf("load tree [%s] failed: %s", id, err)
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf(msg)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
node := treenode.GetNodeInTree(tree, id)
|
|
|
|
|
if nil == node {
|
|
|
|
|
return nil // move 以后的情况,列表项移动导致的状态异常 https://github.com/siyuan-note/insider/issues/961
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-06 11:03:51 +08:00
|
|
|
|
// 收集引用的定义块 ID
|
|
|
|
|
refDefIDs := getRefDefIDs(node)
|
|
|
|
|
// 推送定义节点引用计数
|
|
|
|
|
for _, defID := range refDefIDs {
|
2025-07-17 16:33:22 +08:00
|
|
|
|
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, defID)
|
2024-09-06 11:03:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
parent := node.Parent
|
|
|
|
|
if nil != node.Next && ast.NodeKramdownBlockIAL == node.Next.Type && bytes.Contains(node.Next.Tokens, []byte(node.ID)) {
|
|
|
|
|
// 列表块撤销状态异常 https://github.com/siyuan-note/siyuan/issues/3985
|
|
|
|
|
node.Next.Unlink()
|
|
|
|
|
}
|
2024-01-03 17:38:17 +08:00
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
node.Unlink()
|
|
|
|
|
if nil != parent && ast.NodeListItem == parent.Type && nil == parent.FirstChild {
|
2024-12-30 10:46:22 +08:00
|
|
|
|
needAppendEmptyListItem := true
|
|
|
|
|
for _, op := range tx.DoOperations {
|
|
|
|
|
if "insert" == op.Action && op.ParentID == parent.ID {
|
|
|
|
|
needAppendEmptyListItem = false
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if needAppendEmptyListItem {
|
|
|
|
|
parent.AppendChild(treenode.NewParagraph(ast.NewNodeID()))
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
treenode.RemoveBlockTree(node.ID)
|
|
|
|
|
|
|
|
|
|
delete(tx.nodes, node.ID)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = tx.writeTree(tree); err != nil {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
2023-07-16 00:08:01 +08:00
|
|
|
|
|
2024-08-12 10:51:19 +08:00
|
|
|
|
// 如果是断开列表时的删除列表项事务,则不需要删除数据库绑定块,因为断开列表事务后面会再次插入相同 ID 的列表项
|
|
|
|
|
// List item disconnection no longer affects database binding blocks https://github.com/siyuan-note/siyuan/issues/12235
|
|
|
|
|
needSyncDel2AvBlock := true
|
|
|
|
|
if ast.NodeListItem == node.Type {
|
|
|
|
|
for _, op := range tx.DoOperations {
|
|
|
|
|
// 不可能出现相同 ID 先插入再删除的情况,只可能出现先删除再插入的情况,所以这里只需要查找插入操作
|
|
|
|
|
if "insert" == op.Action {
|
|
|
|
|
data := strings.ReplaceAll(op.Data.(string), editor.FrontEndCaret, "")
|
|
|
|
|
subTree := tx.luteEngine.BlockDOM2Tree(data)
|
|
|
|
|
ast.Walk(subTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
|
|
|
if !entering || ast.NodeListItem != n.Type {
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if n.ID == operation.ID {
|
|
|
|
|
needSyncDel2AvBlock = false
|
|
|
|
|
return ast.WalkStop
|
|
|
|
|
}
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if needSyncDel2AvBlock {
|
2024-09-26 09:00:35 +08:00
|
|
|
|
syncDelete2AvBlock(node, tree, tx)
|
2024-08-12 10:51:19 +08:00
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 09:00:35 +08:00
|
|
|
|
func syncDelete2AvBlock(node *ast.Node, nodeTree *parse.Tree, tx *Transaction) {
|
2024-09-04 20:15:10 +08:00
|
|
|
|
changedAvIDs := syncDelete2AttributeView(node)
|
2024-09-26 09:00:35 +08:00
|
|
|
|
avIDs := tx.syncDelete2Block(node, nodeTree)
|
2024-09-04 20:15:10 +08:00
|
|
|
|
changedAvIDs = append(changedAvIDs, avIDs...)
|
|
|
|
|
changedAvIDs = gulu.Str.RemoveDuplicatedElem(changedAvIDs)
|
|
|
|
|
|
2024-09-05 16:28:42 +08:00
|
|
|
|
for _, avID := range changedAvIDs {
|
|
|
|
|
ReloadAttrView(avID)
|
|
|
|
|
}
|
2024-09-04 20:15:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 09:00:35 +08:00
|
|
|
|
func (tx *Transaction) syncDelete2Block(node *ast.Node, nodeTree *parse.Tree) (changedAvIDs []string) {
|
2023-11-10 23:13:57 +08:00
|
|
|
|
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
|
2024-08-04 11:24:04 +08:00
|
|
|
|
if !entering || ast.NodeAttributeView != n.Type {
|
2023-11-10 23:13:57 +08:00
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-04 11:24:04 +08:00
|
|
|
|
avID := n.AttributeViewID
|
|
|
|
|
isMirror := av.IsMirror(avID)
|
|
|
|
|
if changed := av.RemoveBlockRel(avID, n.ID, treenode.ExistBlockTree); changed {
|
|
|
|
|
changedAvIDs = append(changedAvIDs, avID)
|
|
|
|
|
}
|
2024-07-02 12:35:11 +08:00
|
|
|
|
|
2024-08-04 11:24:04 +08:00
|
|
|
|
if isMirror {
|
|
|
|
|
// 删除镜像数据库节点后不需要解绑块,因为其他镜像节点还在使用
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
2024-07-02 12:35:11 +08:00
|
|
|
|
|
2024-08-04 11:24:04 +08:00
|
|
|
|
attrView, err := av.ParseAttributeView(avID)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2024-08-04 11:24:04 +08:00
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 09:00:35 +08:00
|
|
|
|
trees, nodes := tx.getAttrViewBoundNodes(attrView)
|
2024-08-04 11:24:04 +08:00
|
|
|
|
for _, toChangNode := range nodes {
|
|
|
|
|
avs := toChangNode.IALAttr(av.NodeAttrNameAvs)
|
|
|
|
|
if "" != avs {
|
|
|
|
|
avIDs := strings.Split(avs, ",")
|
|
|
|
|
avIDs = gulu.Str.RemoveElem(avIDs, avID)
|
|
|
|
|
if 1 > len(avIDs) {
|
|
|
|
|
toChangNode.RemoveIALAttr(av.NodeAttrNameAvs)
|
|
|
|
|
} else {
|
|
|
|
|
toChangNode.SetIALAttr(av.NodeAttrNameAvs, strings.Join(avIDs, ","))
|
2024-07-02 12:35:11 +08:00
|
|
|
|
}
|
2024-04-21 18:18:23 +08:00
|
|
|
|
}
|
2024-08-04 11:24:04 +08:00
|
|
|
|
avNames := getAvNames(toChangNode.IALAttr(av.NodeAttrNameAvs))
|
|
|
|
|
oldAttrs := parse.IAL2Map(toChangNode.KramdownIAL)
|
|
|
|
|
toChangNode.SetIALAttr(av.NodeAttrViewNames, avNames)
|
|
|
|
|
pushBroadcastAttrTransactions(oldAttrs, toChangNode)
|
|
|
|
|
}
|
2024-09-26 09:00:35 +08:00
|
|
|
|
|
|
|
|
|
nodeTreeID := nodeTree.ID
|
2024-08-04 11:24:04 +08:00
|
|
|
|
for _, tree := range trees {
|
2024-09-26 09:00:35 +08:00
|
|
|
|
self := nodeTreeID == tree.ID
|
|
|
|
|
if !self {
|
|
|
|
|
indexWriteTreeUpsertQueue(tree)
|
|
|
|
|
}
|
2023-11-10 23:13:57 +08:00
|
|
|
|
}
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
})
|
2024-09-04 20:15:10 +08:00
|
|
|
|
|
2024-07-02 12:35:11 +08:00
|
|
|
|
changedAvIDs = gulu.Str.RemoveDuplicatedElem(changedAvIDs)
|
2024-09-04 20:15:10 +08:00
|
|
|
|
return
|
2023-11-10 23:13:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-09-04 20:15:10 +08:00
|
|
|
|
func syncDelete2AttributeView(node *ast.Node) (changedAvIDs []string) {
|
2024-04-07 17:39:58 +08:00
|
|
|
|
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
|
|
|
if !entering || !n.IsBlock() {
|
|
|
|
|
return ast.WalkContinue
|
2023-07-16 00:08:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-04-07 17:39:58 +08:00
|
|
|
|
avs := n.IALAttr(av.NodeAttrNameAvs)
|
|
|
|
|
if "" == avs {
|
|
|
|
|
return ast.WalkContinue
|
2023-07-16 00:08:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-04-07 17:39:58 +08:00
|
|
|
|
avIDs := strings.Split(avs, ",")
|
|
|
|
|
for _, avID := range avIDs {
|
|
|
|
|
attrView, parseErr := av.ParseAttributeView(avID)
|
|
|
|
|
if nil != parseErr {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
changedAv := false
|
|
|
|
|
blockValues := attrView.GetBlockKeyValues()
|
|
|
|
|
if nil == blockValues {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i, blockValue := range blockValues.Values {
|
|
|
|
|
if blockValue.Block.ID == n.ID {
|
|
|
|
|
blockValues.Values = append(blockValues.Values[:i], blockValues.Values[i+1:]...)
|
|
|
|
|
changedAv = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if changedAv {
|
|
|
|
|
av.SaveAttributeView(attrView)
|
2024-09-04 20:15:10 +08:00
|
|
|
|
changedAvIDs = append(changedAvIDs, avID)
|
2023-07-16 00:08:01 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-07 17:39:58 +08:00
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
})
|
|
|
|
|
|
2024-09-04 20:15:10 +08:00
|
|
|
|
changedAvIDs = gulu.Str.RemoveDuplicatedElem(changedAvIDs)
|
|
|
|
|
return
|
2023-07-16 00:08:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) {
|
2025-07-21 20:14:46 +08:00
|
|
|
|
var bt *treenode.BlockTree
|
|
|
|
|
bts := treenode.GetBlockTrees([]string{operation.ParentID, operation.PreviousID, operation.NextID})
|
|
|
|
|
for _, b := range bts {
|
|
|
|
|
if "" != b.ID {
|
|
|
|
|
bt = b
|
|
|
|
|
break
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-21 20:14:46 +08:00
|
|
|
|
if nil == bt {
|
|
|
|
|
logging.LogWarnf("not found block tree [%s, %s, %s]", operation.ParentID, operation.PreviousID, operation.NextID)
|
2023-09-14 09:31:23 +08:00
|
|
|
|
util.ReloadUI() // 比如分屏后编辑器状态不一致,这里强制重新载入界面
|
|
|
|
|
return
|
2022-10-14 16:46:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 20:14:46 +08:00
|
|
|
|
var err error
|
|
|
|
|
tree, err := tx.loadTreeByBlockTree(bt)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2025-07-21 20:14:46 +08:00
|
|
|
|
msg := fmt.Sprintf("load tree [%s] failed: %s", bt.ID, err)
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf(msg)
|
2025-07-21 20:14:46 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: bt.ID}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-09-14 00:35:13 +08:00
|
|
|
|
data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
|
2023-02-10 14:28:10 +08:00
|
|
|
|
subTree := tx.luteEngine.BlockDOM2Tree(data)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
|
2025-07-21 20:14:46 +08:00
|
|
|
|
if !tx.isGlobalAssetsInit {
|
|
|
|
|
tx.assetsDir = getAssetsDir(filepath.Join(util.DataDir, bt.BoxID), filepath.Dir(filepath.Join(util.DataDir, bt.BoxID, bt.Path)))
|
|
|
|
|
tx.isGlobalAssets = strings.HasPrefix(tx.assetsDir, filepath.Join(util.DataDir, "assets"))
|
|
|
|
|
tx.isGlobalAssetsInit = true
|
|
|
|
|
}
|
|
|
|
|
if !tx.isGlobalAssets {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
// 本地资源文件需要移动到用户手动建立的 assets 下 https://github.com/siyuan-note/siyuan/issues/2410
|
|
|
|
|
ast.Walk(subTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
|
|
|
if !entering {
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ast.NodeLinkDest == n.Type && bytes.HasPrefix(n.Tokens, []byte("assets/")) {
|
|
|
|
|
assetP := gulu.Str.FromBytes(n.Tokens)
|
|
|
|
|
assetPath, e := GetAssetAbsPath(assetP)
|
|
|
|
|
if nil != e {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("get path of asset [%s] failed: %s", assetP, err)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !strings.HasPrefix(assetPath, filepath.Join(util.DataDir, "assets")) {
|
|
|
|
|
// 非全局 assets 则跳过
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 只有全局 assets 才移动到相对 assets
|
2025-07-21 20:14:46 +08:00
|
|
|
|
targetP := filepath.Join(tx.assetsDir, filepath.Base(assetPath))
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if e = filelock.Rename(assetPath, targetP); err != nil {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("copy path of asset from [%s] to [%s] failed: %s", assetPath, targetP, err)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-07-21 20:14:46 +08:00
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
insertedNode := subTree.Root.FirstChild
|
|
|
|
|
if nil == insertedNode {
|
2025-07-21 20:14:46 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: bt.ID}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
var remains []*ast.Node
|
|
|
|
|
for remain := insertedNode.Next; nil != remain; remain = remain.Next {
|
|
|
|
|
if ast.NodeKramdownBlockIAL != remain.Type {
|
|
|
|
|
if "" == remain.ID {
|
|
|
|
|
remain.ID = ast.NewNodeID()
|
|
|
|
|
remain.SetIALAttr("id", remain.ID)
|
|
|
|
|
}
|
|
|
|
|
remains = append(remains, remain)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if "" == insertedNode.ID {
|
|
|
|
|
insertedNode.ID = ast.NewNodeID()
|
|
|
|
|
insertedNode.SetIALAttr("id", insertedNode.ID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var node *ast.Node
|
2022-10-14 16:46:15 +08:00
|
|
|
|
nextID := operation.NextID
|
2022-05-26 15:18:53 +08:00
|
|
|
|
previousID := operation.PreviousID
|
2022-10-14 16:46:15 +08:00
|
|
|
|
if "" != nextID {
|
|
|
|
|
node = treenode.GetNodeInTree(tree, nextID)
|
|
|
|
|
if nil == node {
|
|
|
|
|
logging.LogErrorf("get node [%s] in tree [%s] failed", nextID, tree.Root.ID)
|
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: nextID}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ast.NodeList == insertedNode.Type && nil != node.Parent && ast.NodeList == node.Parent.Type {
|
|
|
|
|
insertedNode = insertedNode.FirstChild
|
|
|
|
|
}
|
|
|
|
|
node.InsertBefore(insertedNode)
|
|
|
|
|
} else if "" != previousID {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
node = treenode.GetNodeInTree(tree, previousID)
|
|
|
|
|
if nil == node {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("get node [%s] in tree [%s] failed", previousID, tree.Root.ID)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: previousID}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ast.NodeHeading == node.Type && "1" == node.IALAttr("fold") {
|
2022-06-19 17:01:21 +08:00
|
|
|
|
children := treenode.HeadingChildren(node)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if l := len(children); 0 < l {
|
|
|
|
|
node = children[l-1]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ast.NodeList == insertedNode.Type && nil != node.Parent && ast.NodeList == node.Parent.Type {
|
|
|
|
|
insertedNode = insertedNode.FirstChild
|
|
|
|
|
}
|
|
|
|
|
for i := len(remains) - 1; 0 <= i; i-- {
|
|
|
|
|
remain := remains[i]
|
|
|
|
|
node.InsertAfter(remain)
|
|
|
|
|
}
|
|
|
|
|
node.InsertAfter(insertedNode)
|
|
|
|
|
} else {
|
|
|
|
|
node = treenode.GetNodeInTree(tree, operation.ParentID)
|
|
|
|
|
if nil == node {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("get node [%s] in tree [%s] failed", operation.ParentID, tree.Root.ID)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
|
|
|
|
|
}
|
|
|
|
|
if ast.NodeSuperBlock == node.Type {
|
|
|
|
|
// 在布局节点后插入
|
|
|
|
|
node.FirstChild.Next.InsertAfter(insertedNode)
|
|
|
|
|
} else {
|
|
|
|
|
if ast.NodeList == insertedNode.Type && nil != insertedNode.FirstChild && operation.ID == insertedNode.FirstChild.ID && operation.ID != insertedNode.ID {
|
|
|
|
|
// 将一个列表项移动到另一个列表的第一项时 https://github.com/siyuan-note/siyuan/issues/2341
|
|
|
|
|
insertedNode = insertedNode.FirstChild
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ast.NodeListItem == node.Type && 3 == node.ListData.Typ {
|
|
|
|
|
// 在任务列表标记节点后插入
|
|
|
|
|
node.FirstChild.InsertAfter(insertedNode)
|
|
|
|
|
for _, remain := range remains {
|
|
|
|
|
node.FirstChild.InsertAfter(remain)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for i := len(remains) - 1; 0 <= i; i-- {
|
|
|
|
|
remain := remains[i]
|
|
|
|
|
node.PrependChild(remain)
|
|
|
|
|
}
|
|
|
|
|
node.PrependChild(insertedNode)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createdUpdated(insertedNode)
|
|
|
|
|
tx.nodes[insertedNode.ID] = insertedNode
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = tx.writeTree(tree); err != nil {
|
2025-07-21 20:14:46 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: bt.ID}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-09-06 11:03:51 +08:00
|
|
|
|
// 收集引用的定义块 ID
|
|
|
|
|
refDefIDs := getRefDefIDs(insertedNode)
|
|
|
|
|
// 推送定义节点引用计数
|
|
|
|
|
for _, defID := range refDefIDs {
|
2025-07-17 16:33:22 +08:00
|
|
|
|
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, defID)
|
2024-09-06 11:03:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-10 23:13:57 +08:00
|
|
|
|
upsertAvBlockRel(insertedNode)
|
2023-11-02 10:32:18 +08:00
|
|
|
|
|
2024-07-17 09:59:22 +08:00
|
|
|
|
// 复制为副本时将该副本块插入到数据库中 https://github.com/siyuan-note/siyuan/issues/11959
|
|
|
|
|
avs := insertedNode.IALAttr(av.NodeAttrNameAvs)
|
|
|
|
|
for _, avID := range strings.Split(avs, ",") {
|
2024-07-24 09:55:08 +08:00
|
|
|
|
if !ast.IsNodeIDPattern(avID) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-17 09:59:22 +08:00
|
|
|
|
AddAttributeViewBlock(tx, []map[string]interface{}{{
|
|
|
|
|
"id": insertedNode.ID,
|
|
|
|
|
"isDetached": false,
|
|
|
|
|
}}, avID, "", previousID, false)
|
2024-09-05 12:10:42 +08:00
|
|
|
|
ReloadAttrView(avID)
|
2024-07-17 09:59:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-11-05 09:18:11 +08:00
|
|
|
|
if ast.NodeAttributeView == insertedNode.Type {
|
2025-07-12 18:05:13 +08:00
|
|
|
|
// 插入数据库块时需要重新绑定其中已经存在的块
|
|
|
|
|
// 比如剪切操作时,会先进行 delete 数据库解绑块,这里需要重新绑定 https://github.com/siyuan-note/siyuan/issues/13031
|
2024-11-05 09:18:11 +08:00
|
|
|
|
attrView, parseErr := av.ParseAttributeView(insertedNode.AttributeViewID)
|
|
|
|
|
if nil == parseErr {
|
|
|
|
|
trees, toBindNodes := tx.getAttrViewBoundNodes(attrView)
|
|
|
|
|
for _, toBindNode := range toBindNodes {
|
|
|
|
|
t := trees[toBindNode.ID]
|
|
|
|
|
bindBlockAv0(tx, insertedNode.AttributeViewID, toBindNode, t)
|
|
|
|
|
}
|
2025-07-12 18:05:13 +08:00
|
|
|
|
|
|
|
|
|
// 设置视图 https://github.com/siyuan-note/siyuan/issues/15279
|
|
|
|
|
v := attrView.GetView(attrView.ViewID)
|
|
|
|
|
if nil != v {
|
|
|
|
|
insertedNode.AttributeViewType = string(v.LayoutType)
|
2025-07-16 22:30:40 +08:00
|
|
|
|
attrs := parse.IAL2Map(insertedNode.KramdownIAL)
|
|
|
|
|
if "" == attrs[av.NodeAttrView] {
|
|
|
|
|
attrs[av.NodeAttrView] = v.ID
|
|
|
|
|
err = setNodeAttrs(insertedNode, tree, attrs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logging.LogWarnf("set node [%s] attrs failed: %s", operation.BlockID, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-12 18:05:13 +08:00
|
|
|
|
}
|
2024-11-05 09:18:11 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
operation.ID = insertedNode.ID
|
|
|
|
|
operation.ParentID = insertedNode.Parent.ID
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (tx *Transaction) doUpdate(operation *Operation) (ret *TxErr) {
|
|
|
|
|
id := operation.ID
|
|
|
|
|
tree, err := tx.loadTree(id)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-01-28 10:24:51 +08:00
|
|
|
|
logging.LogErrorf("load tree [%s] failed: %s", id, err)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-14 00:35:13 +08:00
|
|
|
|
data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
|
2022-05-26 15:18:53 +08:00
|
|
|
|
if "" == data {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("update data is nil")
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-10 14:28:10 +08:00
|
|
|
|
subTree := tx.luteEngine.BlockDOM2Tree(data)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
subTree.ID, subTree.Box, subTree.Path = tree.ID, tree.Box, tree.Path
|
|
|
|
|
oldNode := treenode.GetNodeInTree(tree, id)
|
|
|
|
|
if nil == oldNode {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("get node [%s] in tree [%s] failed", id, tree.Root.ID)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{msg: ErrBlockNotFound.Error(), id: id}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-06 11:03:51 +08:00
|
|
|
|
// 收集引用的定义块 ID
|
2024-09-06 17:04:07 +08:00
|
|
|
|
oldDefIDs := getRefDefIDs(oldNode)
|
|
|
|
|
var newDefIDs []string
|
2024-09-06 11:03:51 +08:00
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
var unlinks []*ast.Node
|
|
|
|
|
ast.Walk(subTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
|
|
|
if !entering {
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-08 20:19:35 +08:00
|
|
|
|
if ast.NodeTextMark == n.Type {
|
2022-09-16 20:50:14 +08:00
|
|
|
|
if n.IsTextMarkType("inline-math") {
|
|
|
|
|
if "" == strings.TrimSpace(n.TextMarkInlineMathContent) {
|
2022-12-08 20:19:35 +08:00
|
|
|
|
// 剔除空白的行级公式
|
2022-09-16 20:50:14 +08:00
|
|
|
|
unlinks = append(unlinks, n)
|
|
|
|
|
}
|
2022-09-26 20:54:22 +08:00
|
|
|
|
} else if n.IsTextMarkType("block-ref") {
|
|
|
|
|
sql.CacheRef(subTree, n)
|
|
|
|
|
|
|
|
|
|
if "d" == n.TextMarkBlockRefSubtype {
|
|
|
|
|
// 偶发编辑文档标题后引用处的动态锚文本不更新 https://github.com/siyuan-note/siyuan/issues/5891
|
|
|
|
|
// 使用缓存的动态锚文本强制覆盖当前块中的引用节点动态锚文本
|
|
|
|
|
if dRefText, ok := treenode.DynamicRefTexts.Load(n.TextMarkBlockRefID); ok && "" != dRefText {
|
|
|
|
|
n.TextMarkTextContent = dRefText.(string)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-06 11:03:51 +08:00
|
|
|
|
|
2024-09-06 17:04:07 +08:00
|
|
|
|
newDefIDs = append(newDefIDs, n.TextMarkBlockRefID)
|
2022-09-16 20:50:14 +08:00
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
})
|
|
|
|
|
for _, n := range unlinks {
|
|
|
|
|
n.Unlink()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-06 17:04:07 +08:00
|
|
|
|
oldDefIDs = gulu.Str.RemoveDuplicatedElem(oldDefIDs)
|
|
|
|
|
newDefIDs = gulu.Str.RemoveDuplicatedElem(newDefIDs)
|
|
|
|
|
refDefIDs := oldDefIDs
|
|
|
|
|
|
|
|
|
|
if !slices.Equal(oldDefIDs, newDefIDs) { // 如果引用发生了变化,则推送定义节点引用计数
|
|
|
|
|
refDefIDs = append(refDefIDs, newDefIDs...)
|
|
|
|
|
refDefIDs = gulu.Str.RemoveDuplicatedElem(refDefIDs)
|
|
|
|
|
for _, defID := range refDefIDs {
|
2025-07-17 16:33:22 +08:00
|
|
|
|
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, defID)
|
2024-09-06 11:03:51 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
updatedNode := subTree.Root.FirstChild
|
|
|
|
|
if nil == updatedNode {
|
2022-07-17 12:22:32 +08:00
|
|
|
|
logging.LogErrorf("get fist node in sub tree [%s] failed", subTree.Root.ID)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return &TxErr{msg: ErrBlockNotFound.Error(), id: id}
|
|
|
|
|
}
|
|
|
|
|
if ast.NodeList == updatedNode.Type && ast.NodeList == oldNode.Parent.Type {
|
|
|
|
|
updatedNode = updatedNode.FirstChild
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if oldNode.IsContainerBlock() {
|
|
|
|
|
// 更新容器块的话需要考虑其子块中可能存在的折叠标题,需要把这些折叠标题的下方块移动到新节点下面
|
|
|
|
|
treenode.MoveFoldHeading(updatedNode, oldNode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cache.PutBlockIAL(updatedNode.ID, parse.IAL2Map(updatedNode.KramdownIAL))
|
|
|
|
|
|
|
|
|
|
// 替换为新节点
|
|
|
|
|
oldNode.InsertAfter(updatedNode)
|
|
|
|
|
oldNode.Unlink()
|
|
|
|
|
|
|
|
|
|
createdUpdated(updatedNode)
|
2024-01-03 10:18:53 +08:00
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
tx.nodes[updatedNode.ID] = updatedNode
|
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: id}
|
|
|
|
|
}
|
2023-11-02 10:32:18 +08:00
|
|
|
|
|
2023-11-10 23:13:57 +08:00
|
|
|
|
upsertAvBlockRel(updatedNode)
|
2023-11-27 22:33:43 +08:00
|
|
|
|
|
2025-07-18 10:01:24 +08:00
|
|
|
|
if ast.NodeAttributeView == updatedNode.Type {
|
|
|
|
|
// 设置视图 https://github.com/siyuan-note/siyuan/issues/15279
|
|
|
|
|
attrView, parseErr := av.ParseAttributeView(updatedNode.AttributeViewID)
|
|
|
|
|
if nil == parseErr {
|
|
|
|
|
v := attrView.GetView(attrView.ViewID)
|
|
|
|
|
if nil != v {
|
|
|
|
|
updatedNode.AttributeViewType = string(v.LayoutType)
|
|
|
|
|
attrs := parse.IAL2Map(updatedNode.KramdownIAL)
|
|
|
|
|
if "" == attrs[av.NodeAttrView] {
|
|
|
|
|
attrs[av.NodeAttrView] = v.ID
|
|
|
|
|
err = setNodeAttrs(updatedNode, tree, attrs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logging.LogWarnf("set node [%s] attrs failed: %s", operation.BlockID, err)
|
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-06 11:03:51 +08:00
|
|
|
|
func getRefDefIDs(node *ast.Node) (refDefIDs []string) {
|
|
|
|
|
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
|
|
|
if !entering {
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-08 11:44:36 +08:00
|
|
|
|
if treenode.IsBlockRef(n) {
|
2024-09-06 11:03:51 +08:00
|
|
|
|
refDefIDs = append(refDefIDs, n.TextMarkBlockRefID)
|
2024-09-15 16:48:35 +08:00
|
|
|
|
} else if treenode.IsEmbedBlockRef(n) {
|
|
|
|
|
defID := treenode.GetEmbedBlockRef(n)
|
|
|
|
|
refDefIDs = append(refDefIDs, defID)
|
2024-09-06 11:03:51 +08:00
|
|
|
|
}
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
})
|
2024-09-15 16:48:35 +08:00
|
|
|
|
refDefIDs = gulu.Str.RemoveDuplicatedElem(refDefIDs)
|
2024-09-06 11:03:51 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-10 23:13:57 +08:00
|
|
|
|
func upsertAvBlockRel(node *ast.Node) {
|
2024-11-02 14:09:50 +08:00
|
|
|
|
var affectedAvIDs []string
|
2023-11-10 23:13:57 +08:00
|
|
|
|
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
|
|
|
if !entering {
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ast.NodeAttributeView == n.Type {
|
|
|
|
|
avID := n.AttributeViewID
|
2024-04-21 18:18:23 +08:00
|
|
|
|
if changed := av.UpsertBlockRel(avID, n.ID); changed {
|
2024-11-02 14:09:50 +08:00
|
|
|
|
affectedAvIDs = append(affectedAvIDs, avID)
|
2024-04-21 18:18:23 +08:00
|
|
|
|
}
|
2023-11-10 23:13:57 +08:00
|
|
|
|
}
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
})
|
2024-11-02 14:09:50 +08:00
|
|
|
|
|
|
|
|
|
updatedNodes := []*ast.Node{node}
|
|
|
|
|
var parents []*ast.Node
|
|
|
|
|
for parent := node.Parent; nil != parent && ast.NodeDocument != parent.Type; parent = parent.Parent {
|
|
|
|
|
parents = append(parents, parent)
|
|
|
|
|
}
|
|
|
|
|
updatedNodes = append(updatedNodes, parents...)
|
|
|
|
|
for _, updatedNode := range updatedNodes {
|
|
|
|
|
ast.Walk(updatedNode, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
|
|
|
avs := n.IALAttr(av.NodeAttrNameAvs)
|
|
|
|
|
if "" == avs {
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
avIDs := strings.Split(avs, ",")
|
|
|
|
|
affectedAvIDs = append(affectedAvIDs, avIDs...)
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-02 14:53:40 +08:00
|
|
|
|
go func() {
|
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
|
sql.FlushQueue()
|
2025-01-05 00:53:55 +08:00
|
|
|
|
|
|
|
|
|
affectedAvIDs = gulu.Str.RemoveDuplicatedElem(affectedAvIDs)
|
|
|
|
|
var relatedAvIDs []string
|
|
|
|
|
for _, avID := range affectedAvIDs {
|
|
|
|
|
relatedAvIDs = append(relatedAvIDs, av.GetSrcAvIDs(avID)...)
|
|
|
|
|
}
|
|
|
|
|
affectedAvIDs = append(affectedAvIDs, relatedAvIDs...)
|
|
|
|
|
affectedAvIDs = gulu.Str.RemoveDuplicatedElem(affectedAvIDs)
|
2024-11-02 14:53:40 +08:00
|
|
|
|
for _, avID := range affectedAvIDs {
|
|
|
|
|
ReloadAttrView(avID)
|
|
|
|
|
}
|
|
|
|
|
}()
|
2023-11-10 23:13:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-10-13 22:50:11 +08:00
|
|
|
|
func (tx *Transaction) doUpdateUpdated(operation *Operation) (ret *TxErr) {
|
|
|
|
|
id := operation.ID
|
|
|
|
|
tree, err := tx.loadTree(id)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-10-13 22:50:11 +08:00
|
|
|
|
if errors.Is(err, ErrBlockNotFound) {
|
|
|
|
|
logging.LogWarnf("not found block [%s]", id)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logging.LogErrorf("load tree [%s] failed: %s", id, err)
|
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
node := treenode.GetNodeInTree(tree, id)
|
|
|
|
|
if nil == node {
|
|
|
|
|
logging.LogErrorf("get node [%s] in tree [%s] failed", id, tree.Root.ID)
|
|
|
|
|
return &TxErr{msg: ErrBlockNotFound.Error(), id: id}
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-13 23:22:17 +08:00
|
|
|
|
node.SetIALAttr("updated", operation.Data.(string))
|
2023-10-13 22:50:11 +08:00
|
|
|
|
createdUpdated(node)
|
|
|
|
|
tx.nodes[node.ID] = node
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = tx.writeTree(tree); err != nil {
|
2023-10-13 22:50:11 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
func (tx *Transaction) doCreate(operation *Operation) (ret *TxErr) {
|
|
|
|
|
tree := operation.Data.(*parse.Tree)
|
|
|
|
|
tx.writeTree(tree)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-09 23:00:09 +08:00
|
|
|
|
func (tx *Transaction) doSetAttrs(operation *Operation) (ret *TxErr) {
|
2022-11-12 00:16:24 +08:00
|
|
|
|
id := operation.ID
|
|
|
|
|
tree, err := tx.loadTree(id)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2023-01-28 10:24:51 +08:00
|
|
|
|
logging.LogErrorf("load tree [%s] failed: %s", id, err)
|
2022-11-12 00:16:24 +08:00
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
node := treenode.GetNodeInTree(tree, id)
|
|
|
|
|
if nil == node {
|
|
|
|
|
logging.LogErrorf("get node [%s] in tree [%s] failed", id, tree.Root.ID)
|
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attrs := map[string]string{}
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = gulu.JSON.UnmarshalJSON([]byte(operation.Data.(string)), &attrs); err != nil {
|
2022-11-12 00:16:24 +08:00
|
|
|
|
logging.LogErrorf("unmarshal attrs failed: %s", err)
|
|
|
|
|
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var invalidNames []string
|
|
|
|
|
for name := range attrs {
|
|
|
|
|
for i := 0; i < len(name); i++ {
|
|
|
|
|
if !lex.IsASCIILetterNumHyphen(name[i]) {
|
|
|
|
|
logging.LogWarnf("invalid attr name [%s]", name)
|
|
|
|
|
invalidNames = append(invalidNames, name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, name := range invalidNames {
|
|
|
|
|
delete(attrs, name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for name, value := range attrs {
|
|
|
|
|
if "" == value {
|
|
|
|
|
node.RemoveIALAttr(name)
|
|
|
|
|
} else {
|
|
|
|
|
node.SetIALAttr(name, value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = tx.writeTree(tree); err != nil {
|
2022-11-12 00:16:24 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cache.PutBlockIAL(id, parse.IAL2Map(node.KramdownIAL))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-24 20:26:15 +08:00
|
|
|
|
func refreshUpdated(node *ast.Node) {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
updated := util.CurrentTimeSecondsStr()
|
2023-06-24 20:26:15 +08:00
|
|
|
|
node.SetIALAttr("updated", updated)
|
2024-05-13 17:16:55 +08:00
|
|
|
|
parents := treenode.ParentNodesWithHeadings(node)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
for _, parent := range parents { // 更新所有父节点的更新时间字段
|
|
|
|
|
parent.SetIALAttr("updated", updated)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-09 15:40:36 +08:00
|
|
|
|
func createdUpdated(node *ast.Node) {
|
2024-08-04 12:02:03 +08:00
|
|
|
|
// 补全子节点的更新时间 Improve block update time filling https://github.com/siyuan-note/siyuan/issues/12182
|
|
|
|
|
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
|
|
|
if !entering || !n.IsBlock() || ast.NodeKramdownBlockIAL == n.Type {
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updated := n.IALAttr("updated")
|
|
|
|
|
if "" == updated && ast.IsNodeIDPattern(n.ID) {
|
|
|
|
|
created := util.TimeFromID(n.ID)
|
2024-08-04 12:25:11 +08:00
|
|
|
|
n.SetIALAttr("updated", created)
|
2024-08-04 12:02:03 +08:00
|
|
|
|
}
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
})
|
|
|
|
|
|
2022-06-09 15:40:36 +08:00
|
|
|
|
created := util.TimeFromID(node.ID)
|
|
|
|
|
updated := node.IALAttr("updated")
|
|
|
|
|
if "" == updated {
|
|
|
|
|
updated = created
|
|
|
|
|
}
|
|
|
|
|
if updated < created {
|
2024-08-04 12:02:03 +08:00
|
|
|
|
updated = created
|
2022-06-09 15:40:36 +08:00
|
|
|
|
}
|
2024-05-13 17:16:55 +08:00
|
|
|
|
parents := treenode.ParentNodesWithHeadings(node)
|
2022-06-09 15:40:36 +08:00
|
|
|
|
for _, parent := range parents { // 更新所有父节点的更新时间字段
|
|
|
|
|
parent.SetIALAttr("updated", updated)
|
2024-04-15 20:56:46 +08:00
|
|
|
|
cache.PutBlockIAL(parent.ID, parse.IAL2Map(parent.KramdownIAL))
|
2022-06-09 15:40:36 +08:00
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Operation struct {
|
|
|
|
|
Action string `json:"action"`
|
|
|
|
|
Data interface{} `json:"data"`
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
ParentID string `json:"parentID"`
|
|
|
|
|
PreviousID string `json:"previousID"`
|
2022-10-14 16:46:15 +08:00
|
|
|
|
NextID string `json:"nextID"`
|
2022-05-26 15:18:53 +08:00
|
|
|
|
RetData interface{} `json:"retData"`
|
2023-04-09 23:00:09 +08:00
|
|
|
|
BlockIDs []string `json:"blockIDs"`
|
2024-03-04 16:41:41 +08:00
|
|
|
|
BlockID string `json:"blockID"`
|
2023-04-09 23:00:09 +08:00
|
|
|
|
|
|
|
|
|
DeckID string `json:"deckID"` // 用于添加/删除闪卡
|
2022-05-26 15:18:53 +08:00
|
|
|
|
|
2024-04-20 12:07:17 +08:00
|
|
|
|
AvID string `json:"avID"` // 属性视图 ID
|
2024-04-20 22:37:39 +08:00
|
|
|
|
SrcIDs []string `json:"srcIDs"` // 用于从属性视图中删除行
|
|
|
|
|
Srcs []map[string]interface{} `json:"srcs"` // 用于添加属性视图行(包括绑定块){id, content, isDetached}
|
2024-04-20 12:07:17 +08:00
|
|
|
|
IsDetached bool `json:"isDetached"` // 用于标识是否未绑定块,仅存在于属性视图中
|
|
|
|
|
IgnoreFillFilterVal bool `json:"ignoreFillFilter"` // 用于标识是否忽略填充筛选值
|
|
|
|
|
Name string `json:"name"` // 属性视图列名
|
|
|
|
|
Typ string `json:"type"` // 属性视图列类型
|
|
|
|
|
Format string `json:"format"` // 属性视图列格式化
|
2025-07-11 10:43:09 +08:00
|
|
|
|
KeyID string `json:"keyID"` // 属性视图字段 ID
|
2024-04-20 12:07:17 +08:00
|
|
|
|
RowID string `json:"rowID"` // 属性视图行 ID
|
|
|
|
|
IsTwoWay bool `json:"isTwoWay"` // 属性视图关联列是否是双向关系
|
|
|
|
|
BackRelationKeyID string `json:"backRelationKeyID"` // 属性视图关联列回链关联列的 ID
|
2024-10-17 16:53:18 +08:00
|
|
|
|
RemoveDest bool `json:"removeDest"` // 属性视图删除关联目标
|
2025-06-08 11:01:21 +08:00
|
|
|
|
Layout av.LayoutType `json:"layout"` // 属性视图布局类型
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Transaction struct {
|
2023-09-14 00:51:20 +08:00
|
|
|
|
Timestamp int64 `json:"timestamp"`
|
2022-05-26 15:18:53 +08:00
|
|
|
|
DoOperations []*Operation `json:"doOperations"`
|
|
|
|
|
UndoOperations []*Operation `json:"undoOperations"`
|
|
|
|
|
|
2025-07-21 20:14:46 +08:00
|
|
|
|
trees map[string]*parse.Tree // 事务中变更的树
|
|
|
|
|
nodes map[string]*ast.Node // 事务中变更的节点
|
|
|
|
|
|
|
|
|
|
isGlobalAssetsInit bool // 是否初始化过全局资源判断
|
|
|
|
|
isGlobalAssets bool // 是否属于全局资源
|
|
|
|
|
assetsDir string // 资源目录路径
|
2023-02-10 14:28:10 +08:00
|
|
|
|
|
|
|
|
|
luteEngine *lute.Lute
|
2023-12-08 20:28:57 +08:00
|
|
|
|
m *sync.Mutex
|
2024-02-07 23:37:06 +08:00
|
|
|
|
state atomic.Int32 // 0: 初始化,1:未提交,:2: 已提交,3: 已回滚
|
2023-12-08 20:28:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (tx *Transaction) WaitForCommit() {
|
|
|
|
|
for {
|
2024-02-07 23:37:06 +08:00
|
|
|
|
if 1 == tx.state.Load() {
|
2023-12-08 20:28:57 +08:00
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (tx *Transaction) begin() (err error) {
|
|
|
|
|
tx.trees = map[string]*parse.Tree{}
|
|
|
|
|
tx.nodes = map[string]*ast.Node{}
|
2023-02-10 14:28:10 +08:00
|
|
|
|
tx.luteEngine = util.NewLute()
|
2023-12-08 20:28:57 +08:00
|
|
|
|
tx.m.Lock()
|
2024-02-07 23:37:06 +08:00
|
|
|
|
tx.state.Store(1)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (tx *Transaction) commit() (err error) {
|
|
|
|
|
for _, tree := range tx.trees {
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err = writeTreeUpsertQueue(tree); err != nil {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
2024-04-08 10:49:47 +08:00
|
|
|
|
|
|
|
|
|
var sources []interface{}
|
2024-04-08 20:24:59 +08:00
|
|
|
|
sources = append(sources, tx)
|
2024-04-08 10:49:47 +08:00
|
|
|
|
util.PushSaveDoc(tree.ID, "tx", sources)
|
2025-07-21 20:14:46 +08:00
|
|
|
|
|
|
|
|
|
checkUpsertInUserGuide(tree)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
2023-06-24 17:47:16 +08:00
|
|
|
|
refreshDynamicRefTexts(tx.nodes, tx.trees)
|
2022-07-14 21:50:46 +08:00
|
|
|
|
IncSync()
|
2024-02-07 23:37:06 +08:00
|
|
|
|
tx.state.Store(2)
|
2023-12-08 20:28:57 +08:00
|
|
|
|
tx.m.Unlock()
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (tx *Transaction) rollback() {
|
|
|
|
|
tx.trees, tx.nodes = nil, nil
|
2024-02-07 23:37:06 +08:00
|
|
|
|
tx.state.Store(3)
|
2023-12-08 20:28:57 +08:00
|
|
|
|
tx.m.Unlock()
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 20:14:46 +08:00
|
|
|
|
func (tx *Transaction) loadTreeByBlockTree(bt *treenode.BlockTree) (ret *parse.Tree, err error) {
|
|
|
|
|
if nil == bt {
|
|
|
|
|
return nil, ErrBlockNotFound
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ret = tx.trees[bt.RootID]
|
|
|
|
|
if nil != ret {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ret, err = filesys.LoadTree(bt.BoxID, bt.Path, tx.luteEngine)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
tx.trees[bt.RootID] = ret
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-26 15:18:53 +08:00
|
|
|
|
func (tx *Transaction) loadTree(id string) (ret *parse.Tree, err error) {
|
|
|
|
|
var rootID, box, p string
|
|
|
|
|
bt := treenode.GetBlockTree(id)
|
|
|
|
|
if nil == bt {
|
|
|
|
|
return nil, ErrBlockNotFound
|
|
|
|
|
}
|
|
|
|
|
rootID = bt.RootID
|
|
|
|
|
box = bt.BoxID
|
|
|
|
|
p = bt.Path
|
|
|
|
|
|
|
|
|
|
ret = tx.trees[rootID]
|
|
|
|
|
if nil != ret {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-10 14:28:10 +08:00
|
|
|
|
ret, err = filesys.LoadTree(box, p, tx.luteEngine)
|
2024-09-04 04:40:50 +03:00
|
|
|
|
if err != nil {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
tx.trees[rootID] = ret
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (tx *Transaction) writeTree(tree *parse.Tree) (err error) {
|
|
|
|
|
tx.trees[tree.ID] = tree
|
2024-06-20 22:53:27 +08:00
|
|
|
|
treenode.UpsertBlockTree(tree)
|
2022-05-26 15:18:53 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-17 21:02:25 +08:00
|
|
|
|
func getRefsCacheByDefNode(updateNode *ast.Node) (ret []*sql.Ref, changedNodes []*ast.Node) {
|
|
|
|
|
changedNodesMap := map[string]*ast.Node{}
|
2024-09-06 01:03:21 +08:00
|
|
|
|
ret = sql.GetRefsCacheByDefID(updateNode.ID)
|
|
|
|
|
if nil != updateNode.Parent && ast.NodeDocument != updateNode.Parent.Type &&
|
2025-07-17 12:00:07 +08:00
|
|
|
|
updateNode.Parent.IsContainerBlock() && updateNode == treenode.FirstLeafBlock(updateNode.Parent) {
|
2024-09-06 01:03:21 +08:00
|
|
|
|
// 如果是容器块下第一个叶子块,则需要向上查找引用
|
|
|
|
|
for parent := updateNode.Parent; nil != parent; parent = parent.Parent {
|
|
|
|
|
if ast.NodeDocument == parent.Type {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parentRefs := sql.GetRefsCacheByDefID(parent.ID)
|
|
|
|
|
if 0 < len(parentRefs) {
|
|
|
|
|
ret = append(ret, parentRefs...)
|
2025-07-17 21:02:25 +08:00
|
|
|
|
if _, ok := changedNodesMap[parent.ID]; !ok {
|
|
|
|
|
changedNodesMap[parent.ID] = parent
|
|
|
|
|
}
|
2024-09-06 01:03:21 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-17 12:00:07 +08:00
|
|
|
|
if ast.NodeDocument != updateNode.Type && updateNode.IsContainerBlock() {
|
|
|
|
|
// 如果是容器块,则需要向下查找引用
|
|
|
|
|
ast.Walk(updateNode, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
|
|
|
if !entering || !n.IsBlock() {
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
childRefs := sql.GetRefsCacheByDefID(n.ID)
|
|
|
|
|
if 0 < len(childRefs) {
|
|
|
|
|
ret = append(ret, childRefs...)
|
2025-07-17 21:02:25 +08:00
|
|
|
|
changedNodesMap[n.ID] = n
|
2025-07-17 12:00:07 +08:00
|
|
|
|
}
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-07-17 21:02:25 +08:00
|
|
|
|
if ast.NodeHeading == updateNode.Type && "1" == updateNode.IALAttr("fold") {
|
|
|
|
|
// 如果是折叠标题,则需要向下查找引用
|
|
|
|
|
children := treenode.HeadingChildren(updateNode)
|
|
|
|
|
for _, child := range children {
|
|
|
|
|
childRefs := sql.GetRefsCacheByDefID(child.ID)
|
|
|
|
|
if 0 < len(childRefs) {
|
|
|
|
|
ret = append(ret, childRefs...)
|
|
|
|
|
changedNodesMap[child.ID] = child
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, n := range changedNodesMap {
|
|
|
|
|
changedNodes = append(changedNodes, n)
|
|
|
|
|
}
|
2024-09-06 01:03:21 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-24 17:43:21 +08:00
|
|
|
|
var updateRefTextRenameDocs = map[string]*parse.Tree{}
|
|
|
|
|
var updateRefTextRenameDocLock = sync.Mutex{}
|
|
|
|
|
|
|
|
|
|
func updateRefTextRenameDoc(renamedTree *parse.Tree) {
|
|
|
|
|
updateRefTextRenameDocLock.Lock()
|
|
|
|
|
updateRefTextRenameDocs[renamedTree.ID] = renamedTree
|
|
|
|
|
updateRefTextRenameDocLock.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-26 23:30:29 +08:00
|
|
|
|
func FlushUpdateRefTextRenameDocJob() {
|
2024-10-14 20:39:16 +08:00
|
|
|
|
sql.FlushQueue()
|
2023-01-26 23:30:29 +08:00
|
|
|
|
flushUpdateRefTextRenameDoc()
|
2022-10-24 17:43:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func flushUpdateRefTextRenameDoc() {
|
|
|
|
|
updateRefTextRenameDocLock.Lock()
|
|
|
|
|
defer updateRefTextRenameDocLock.Unlock()
|
|
|
|
|
|
|
|
|
|
for _, tree := range updateRefTextRenameDocs {
|
2023-06-24 17:47:16 +08:00
|
|
|
|
refreshDynamicRefText(tree.Root, tree)
|
2022-10-24 17:43:21 +08:00
|
|
|
|
}
|
|
|
|
|
updateRefTextRenameDocs = map[string]*parse.Tree{}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-06 09:49:46 +08:00
|
|
|
|
type changedDefNode struct {
|
|
|
|
|
id string
|
|
|
|
|
refText string
|
2024-09-08 11:44:36 +08:00
|
|
|
|
refType string // ref-d/ref-s/embed
|
2024-09-06 09:49:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateRefText(refNode *ast.Node, changedDefNodes map[string]*ast.Node) (changed bool, defNodes []*changedDefNode) {
|
2022-05-26 15:18:53 +08:00
|
|
|
|
ast.Walk(refNode, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
|
|
|
if !entering {
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
2024-09-08 11:44:36 +08:00
|
|
|
|
if treenode.IsBlockRef(n) {
|
|
|
|
|
defID, refText, subtype := treenode.GetBlockRef(n)
|
|
|
|
|
if "" == defID {
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
}
|
2022-09-16 18:02:04 +08:00
|
|
|
|
|
2024-09-08 11:44:36 +08:00
|
|
|
|
defNode := changedDefNodes[defID]
|
|
|
|
|
if nil == defNode {
|
|
|
|
|
return ast.WalkSkipChildren
|
|
|
|
|
}
|
2022-09-16 18:02:04 +08:00
|
|
|
|
|
2024-09-08 11:44:36 +08:00
|
|
|
|
changed = true
|
|
|
|
|
if "d" == subtype {
|
2024-12-25 17:38:54 +08:00
|
|
|
|
refText = strings.TrimSpace(getNodeRefText(defNode))
|
|
|
|
|
if "" == refText {
|
|
|
|
|
refText = n.TextMarkBlockRefID
|
|
|
|
|
}
|
2024-09-08 11:44:36 +08:00
|
|
|
|
treenode.SetDynamicBlockRefText(n, refText)
|
|
|
|
|
}
|
|
|
|
|
defNodes = append(defNodes, &changedDefNode{id: defID, refText: refText, refType: "ref-" + subtype})
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
} else if treenode.IsEmbedBlockRef(n) {
|
|
|
|
|
defID := treenode.GetEmbedBlockRef(n)
|
|
|
|
|
changed = true
|
|
|
|
|
defNodes = append(defNodes, &changedDefNode{id: defID, refType: "embed"})
|
|
|
|
|
return ast.WalkContinue
|
2022-05-26 15:18:53 +08:00
|
|
|
|
}
|
|
|
|
|
return ast.WalkContinue
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-11-27 22:33:43 +08:00
|
|
|
|
|
|
|
|
|
func checkUpsertInUserGuide(tree *parse.Tree) {
|
|
|
|
|
// In production mode, data reset warning pops up when editing data in the user guide https://github.com/siyuan-note/siyuan/issues/9757
|
|
|
|
|
if "prod" == util.Mode && IsUserGuide(tree.Box) {
|
|
|
|
|
util.PushErrMsg(Conf.Language(52), 7000)
|
|
|
|
|
}
|
|
|
|
|
}
|