A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
1// SiYuan - Refactor your thinking
2// Copyright (c) 2020-present, b3log.org
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17package model
18
19import (
20 "bytes"
21 "errors"
22 "fmt"
23 "path/filepath"
24 "slices"
25 "strconv"
26 "strings"
27 "sync"
28 "sync/atomic"
29 "time"
30
31 "github.com/88250/gulu"
32 "github.com/88250/lute"
33 "github.com/88250/lute/ast"
34 "github.com/88250/lute/editor"
35 "github.com/88250/lute/lex"
36 "github.com/88250/lute/parse"
37 "github.com/siyuan-note/filelock"
38 "github.com/siyuan-note/logging"
39 "github.com/siyuan-note/siyuan/kernel/av"
40 "github.com/siyuan-note/siyuan/kernel/cache"
41 "github.com/siyuan-note/siyuan/kernel/filesys"
42 "github.com/siyuan-note/siyuan/kernel/sql"
43 "github.com/siyuan-note/siyuan/kernel/task"
44 "github.com/siyuan-note/siyuan/kernel/treenode"
45 "github.com/siyuan-note/siyuan/kernel/util"
46)
47
48func IsMoveOutlineHeading(transactions *[]*Transaction) bool {
49 for _, tx := range *transactions {
50 for _, op := range tx.DoOperations {
51 if "moveOutlineHeading" == op.Action {
52 return true
53 }
54 }
55 }
56 return false
57}
58
59func FlushTxQueue() {
60 time.Sleep(time.Duration(50) * time.Millisecond)
61 for 0 < len(txQueue) || isFlushing {
62 time.Sleep(10 * time.Millisecond)
63 }
64}
65
66var (
67 txQueue = make(chan *Transaction, 7)
68 flushLock = sync.Mutex{}
69 isFlushing = false
70)
71
72func init() {
73 go flushQueue()
74}
75
76func flushQueue() {
77 for {
78 select {
79 case tx := <-txQueue:
80 flushTx(tx)
81 }
82 }
83}
84
85func flushTx(tx *Transaction) {
86 defer logging.Recover()
87 flushLock.Lock()
88 isFlushing = true
89 defer func() {
90 isFlushing = false
91 flushLock.Unlock()
92 }()
93
94 start := time.Now()
95 if txErr := performTx(tx); nil != txErr {
96 switch txErr.code {
97 case TxErrCodeBlockNotFound:
98 util.PushTxErr("Transaction failed", txErr.code, nil)
99 return
100 case TxErrCodeDataIsSyncing:
101 util.PushMsg(Conf.Language(222), 5000)
102 case TxErrHandleAttributeView:
103 util.PushMsg(Conf.language(258), 5000)
104 logging.LogErrorf("handle attribute view failed: %s", txErr.msg)
105 default:
106 txData, _ := gulu.JSON.MarshalJSON(tx)
107 logging.LogFatalf(logging.ExitCodeFatal, "transaction failed [%d]: %s\n tx [%s]", txErr.code, txErr.msg, txData)
108 }
109 }
110 elapsed := time.Now().Sub(start).Milliseconds()
111 if 0 < len(tx.DoOperations) {
112 if 2000 < elapsed {
113 logging.LogWarnf("op tx [%dms]", elapsed)
114 }
115 }
116}
117
118func PerformTransactions(transactions *[]*Transaction) {
119 for _, tx := range *transactions {
120 tx.m = &sync.Mutex{}
121 txQueue <- tx
122 }
123 return
124}
125
126const (
127 TxErrCodeBlockNotFound = 0
128 TxErrCodeDataIsSyncing = 1
129 TxErrCodeWriteTree = 2
130 TxErrHandleAttributeView = 3
131)
132
133type TxErr struct {
134 code int
135 msg string
136 id string
137}
138
139func performTx(tx *Transaction) (ret *TxErr) {
140 if 1 > len(tx.DoOperations) {
141 return
142 }
143
144 //os.MkdirAll("pprof", 0755)
145 //cpuProfile, _ := os.Create("pprof/cpu_profile_tx")
146 //pprof.StartCPUProfile(cpuProfile)
147 //defer pprof.StopCPUProfile()
148
149 var err error
150 if err = tx.begin(); err != nil {
151 if strings.Contains(err.Error(), "database is closed") {
152 return
153 }
154 logging.LogErrorf("begin tx failed: %s", err)
155 ret = &TxErr{msg: err.Error()}
156 return
157 }
158
159 defer func() {
160 if e := recover(); nil != e {
161 msg := fmt.Sprintf("PANIC RECOVERED: %v\n\t%s\n", e, logging.ShortStack())
162 logging.LogErrorf(msg)
163
164 if 1 == tx.state.Load() {
165 tx.rollback()
166 return
167 }
168 }
169 }()
170
171 isLargeInsert := tx.processLargeInsert()
172 if !isLargeInsert {
173 tx.processUndoInsertWithFoldedHeading()
174 for _, op := range tx.DoOperations {
175 switch op.Action {
176 case "create":
177 ret = tx.doCreate(op)
178 case "update":
179 ret = tx.doUpdate(op)
180 case "insert":
181 ret = tx.doInsert(op)
182 case "delete":
183 ret = tx.doDelete(op)
184 case "move":
185 ret = tx.doMove(op)
186 case "moveOutlineHeading":
187 ret = tx.doMoveOutlineHeading(op)
188 case "append":
189 ret = tx.doAppend(op)
190 case "appendInsert":
191 ret = tx.doAppendInsert(op)
192 case "prependInsert":
193 ret = tx.doPrependInsert(op)
194 case "foldHeading":
195 ret = tx.doFoldHeading(op)
196 case "unfoldHeading":
197 ret = tx.doUnfoldHeading(op)
198 case "setAttrs":
199 ret = tx.doSetAttrs(op)
200 case "doUpdateUpdated":
201 ret = tx.doUpdateUpdated(op)
202 case "addFlashcards":
203 ret = tx.doAddFlashcards(op)
204 case "removeFlashcards":
205 ret = tx.doRemoveFlashcards(op)
206 case "setAttrViewName":
207 ret = tx.doSetAttrViewName(op)
208 case "setAttrViewFilters":
209 ret = tx.doSetAttrViewFilters(op)
210 case "setAttrViewSorts":
211 ret = tx.doSetAttrViewSorts(op)
212 case "setAttrViewPageSize":
213 ret = tx.doSetAttrViewPageSize(op)
214 case "setAttrViewColWidth":
215 ret = tx.doSetAttrViewColumnWidth(op)
216 case "setAttrViewColWrap":
217 ret = tx.doSetAttrViewColumnWrap(op)
218 case "setAttrViewColHidden":
219 ret = tx.doSetAttrViewColumnHidden(op)
220 case "setAttrViewColPin":
221 ret = tx.doSetAttrViewColumnPin(op)
222 case "setAttrViewColIcon":
223 ret = tx.doSetAttrViewColumnIcon(op)
224 case "setAttrViewColDesc":
225 ret = tx.doSetAttrViewColumnDesc(op)
226 case "insertAttrViewBlock":
227 ret = tx.doInsertAttrViewBlock(op)
228 case "removeAttrViewBlock":
229 ret = tx.doRemoveAttrViewBlock(op)
230 case "addAttrViewCol":
231 ret = tx.doAddAttrViewColumn(op)
232 case "updateAttrViewCol":
233 ret = tx.doUpdateAttrViewColumn(op)
234 case "removeAttrViewCol":
235 ret = tx.doRemoveAttrViewColumn(op)
236 case "sortAttrViewRow":
237 ret = tx.doSortAttrViewRow(op)
238 case "sortAttrViewCol":
239 ret = tx.doSortAttrViewColumn(op)
240 case "sortAttrViewKey":
241 ret = tx.doSortAttrViewKey(op)
242 case "updateAttrViewCell":
243 ret = tx.doUpdateAttrViewCell(op)
244 case "updateAttrViewColOptions":
245 ret = tx.doUpdateAttrViewColOptions(op)
246 case "removeAttrViewColOption":
247 ret = tx.doRemoveAttrViewColOption(op)
248 case "updateAttrViewColOption":
249 ret = tx.doUpdateAttrViewColOption(op)
250 case "setAttrViewColOptionDesc":
251 ret = tx.doSetAttrViewColOptionDesc(op)
252 case "setAttrViewColCalc":
253 ret = tx.doSetAttrViewColCalc(op)
254 case "updateAttrViewColNumberFormat":
255 ret = tx.doUpdateAttrViewColNumberFormat(op)
256 case "replaceAttrViewBlock":
257 ret = tx.doReplaceAttrViewBlock(op)
258 case "updateAttrViewColTemplate":
259 ret = tx.doUpdateAttrViewColTemplate(op)
260 case "addAttrViewView":
261 ret = tx.doAddAttrViewView(op)
262 case "removeAttrViewView":
263 ret = tx.doRemoveAttrViewView(op)
264 case "setAttrViewViewName":
265 ret = tx.doSetAttrViewViewName(op)
266 case "setAttrViewViewIcon":
267 ret = tx.doSetAttrViewViewIcon(op)
268 case "setAttrViewViewDesc":
269 ret = tx.doSetAttrViewViewDesc(op)
270 case "duplicateAttrViewView":
271 ret = tx.doDuplicateAttrViewView(op)
272 case "sortAttrViewView":
273 ret = tx.doSortAttrViewView(op)
274 case "updateAttrViewColRelation":
275 ret = tx.doUpdateAttrViewColRelation(op)
276 case "updateAttrViewColRollup":
277 ret = tx.doUpdateAttrViewColRollup(op)
278 case "hideAttrViewName":
279 ret = tx.doHideAttrViewName(op)
280 case "setAttrViewColDateFillCreated":
281 ret = tx.doSetAttrViewColDateFillCreated(op)
282 case "setAttrViewColDateFillSpecificTime":
283 ret = tx.doSetAttrViewColDateFillSpecificTime(op)
284 case "duplicateAttrViewKey":
285 ret = tx.doDuplicateAttrViewKey(op)
286 case "setAttrViewCoverFrom":
287 ret = tx.doSetAttrViewCoverFrom(op)
288 case "setAttrViewCoverFromAssetKeyID":
289 ret = tx.doSetAttrViewCoverFromAssetKeyID(op)
290 case "setAttrViewCardSize":
291 ret = tx.doSetAttrViewCardSize(op)
292 case "setAttrViewFitImage":
293 ret = tx.doSetAttrViewFitImage(op)
294 case "setAttrViewDisplayFieldName":
295 ret = tx.doSetAttrViewDisplayFieldName(op)
296 case "setAttrViewShowIcon":
297 ret = tx.doSetAttrViewShowIcon(op)
298 case "setAttrViewWrapField":
299 ret = tx.doSetAttrViewWrapField(op)
300 case "changeAttrViewLayout":
301 ret = tx.doChangeAttrViewLayout(op)
302 case "setAttrViewBlockView":
303 ret = tx.doSetAttrViewBlockView(op)
304 case "setAttrViewCardAspectRatio":
305 ret = tx.doSetAttrViewCardAspectRatio(op)
306 case "setAttrViewGroup":
307 ret = tx.doSetAttrViewGroup(op)
308 case "hideAttrViewGroup":
309 ret = tx.doHideAttrViewGroup(op)
310 case "hideAttrViewAllGroups":
311 ret = tx.doHideAttrViewAllGroups(op)
312 case "foldAttrViewGroup":
313 ret = tx.doFoldAttrViewGroup(op)
314 case "syncAttrViewTableColWidth":
315 ret = tx.doSyncAttrViewTableColWidth(op)
316 case "removeAttrViewGroup":
317 ret = tx.doRemoveAttrViewGroup(op)
318 case "sortAttrViewGroup":
319 ret = tx.doSortAttrViewGroup(op)
320 }
321
322 if nil != ret {
323 tx.rollback()
324 return
325 }
326 }
327 }
328
329 if cr := tx.commit(); nil != cr {
330 logging.LogErrorf("commit tx failed: %s", cr)
331 return &TxErr{msg: cr.Error()}
332 }
333 return
334}
335
336func (tx *Transaction) processUndoInsertWithFoldedHeading() {
337 // 删除折叠标题后撤销,需要调整 insert 顺序和 previousID
338 // https://github.com/siyuan-note/siyuan/issues/16120
339
340 // 所有操作均为 insert 才处理
341 for _, op := range tx.DoOperations {
342 if "insert" != op.Action {
343 return
344 }
345 }
346
347 for i := 0; i < len(tx.DoOperations); i++ {
348 op := tx.DoOperations[i]
349 ignoreProcess := false
350 if nil != op.Context["ignoreProcess"] {
351 var convErr error
352 ignoreProcess, convErr = strconv.ParseBool(op.Context["ignoreProcess"].(string))
353 if nil != convErr {
354 logging.LogErrorf("parse ignoreProcess failed: %s", convErr)
355 return
356 }
357 }
358 if ignoreProcess {
359 // 找到从当前 i 到下个 ignoreProcess=false 的区间,整体反转
360 j := i + 1
361 if j >= len(tx.DoOperations) {
362 return
363 }
364
365 for ; j < len(tx.DoOperations); j++ {
366 nextOp := tx.DoOperations[j]
367 nextIgnoreProcess := false
368 if nil != nextOp.Context["ignoreProcess"] {
369 var convErr error
370 nextIgnoreProcess, convErr = strconv.ParseBool(nextOp.Context["ignoreProcess"].(string))
371 if nil != convErr {
372 logging.LogErrorf("parse ignoreProcess failed: %s", convErr)
373 return
374 }
375 }
376 if !nextIgnoreProcess {
377 break
378 }
379 }
380 for _, nextOp := range tx.DoOperations[i:j] {
381 nextOp.PreviousID = tx.DoOperations[j].ID
382 }
383 slices.Reverse(tx.DoOperations[i : j+1])
384 i = j
385 }
386 if i >= len(tx.DoOperations) {
387 return
388 }
389 }
390}
391
392func (tx *Transaction) processLargeInsert() bool {
393 opSize := len(tx.DoOperations)
394 isLargeInsert := 128 < opSize
395 if isLargeInsert {
396 var previousID string
397 for i, op := range tx.DoOperations {
398 if i == opSize-1 {
399 if "delete" != op.Action {
400 // 最后一个是 delete
401 isLargeInsert = false
402 }
403 break
404 }
405 if "insert" != op.Action {
406 isLargeInsert = false
407 break
408 }
409
410 if "" == op.PreviousID {
411 isLargeInsert = false
412 break
413 }
414 if "" == previousID {
415 previousID = op.PreviousID
416 } else if previousID != op.PreviousID {
417 isLargeInsert = false
418 break
419 }
420 }
421 if isLargeInsert {
422 tx.doLargeInsert(previousID)
423 }
424 }
425 return isLargeInsert
426}
427
428func (tx *Transaction) doMove(operation *Operation) (ret *TxErr) {
429 var err error
430 id := operation.ID
431 srcTree, err := tx.loadTree(id)
432 if err != nil {
433 logging.LogErrorf("load tree [%s] failed: %s", id, err)
434 return &TxErr{code: TxErrCodeBlockNotFound, id: id}
435 }
436
437 srcNode := treenode.GetNodeInTree(srcTree, id)
438 if nil == srcNode {
439 logging.LogErrorf("get node [%s] in tree [%s] failed", id, srcTree.Root.ID)
440 return &TxErr{code: TxErrCodeBlockNotFound, id: id}
441 }
442
443 // 生成文档历史 https://github.com/siyuan-note/siyuan/issues/14359
444 generateOpTypeHistory(srcTree, HistoryOpUpdate)
445
446 var headingChildren []*ast.Node
447 if isMovingFoldHeading := ast.NodeHeading == srcNode.Type && "1" == srcNode.IALAttr("fold"); isMovingFoldHeading {
448 headingChildren = treenode.HeadingChildren(srcNode)
449 // Blocks below other non-folded headings are no longer moved when moving a folded heading https://github.com/siyuan-note/siyuan/issues/8321
450 headingChildren = treenode.GetHeadingFold(headingChildren)
451 }
452
453 var srcEmptyList *ast.Node
454 if ast.NodeListItem == srcNode.Type && srcNode.Parent.FirstChild == srcNode && srcNode.Parent.LastChild == srcNode {
455 // 列表中唯一的列表项被移除后,该列表就为空了
456 srcEmptyList = srcNode.Parent
457 }
458
459 targetPreviousID := operation.PreviousID
460 targetParentID := operation.ParentID
461 if "" != targetPreviousID {
462 if id == targetPreviousID {
463 return
464 }
465
466 var targetTree *parse.Tree
467 targetTree, err = tx.loadTree(targetPreviousID)
468 if err != nil {
469 logging.LogErrorf("load tree [%s] failed: %s", targetPreviousID, err)
470 return &TxErr{code: TxErrCodeBlockNotFound, id: targetPreviousID}
471 }
472 isSameTree := srcTree.ID == targetTree.ID
473 if isSameTree {
474 targetTree = srcTree
475 }
476
477 targetNode := treenode.GetNodeInTree(targetTree, targetPreviousID)
478 if nil == targetNode {
479 logging.LogErrorf("get node [%s] in tree [%s] failed", targetPreviousID, targetTree.Root.ID)
480 return &TxErr{code: TxErrCodeBlockNotFound, id: targetPreviousID}
481 }
482
483 if ast.NodeHeading == targetNode.Type && "1" == targetNode.IALAttr("fold") {
484 targetChildren := treenode.HeadingChildren(targetNode)
485 targetChildren = treenode.GetHeadingFold(targetChildren)
486
487 if l := len(targetChildren); 0 < l {
488 targetNode = targetChildren[l-1]
489 }
490 }
491
492 if isMovingFoldHeadingIntoSelf(targetNode, headingChildren) {
493 return
494 }
495
496 if isMovingParentIntoChild(srcNode, targetNode) {
497 return
498 }
499
500 if 0 < len(headingChildren) {
501 // 折叠标题再编辑形成外层列表(前面加上 * )时,前端给的 tx 序列会形成死循环,在这里解开
502 // Nested lists cause hang after collapsing headings https://github.com/siyuan-note/siyuan/issues/15943
503 lastChild := headingChildren[len(headingChildren)-1]
504 if "1" == lastChild.IALAttr("heading-fold") && ast.NodeList == lastChild.Type &&
505 nil != lastChild.FirstChild && nil != lastChild.FirstChild.FirstChild && lastChild.FirstChild.FirstChild.ID == targetPreviousID {
506 ast.Walk(lastChild, func(n *ast.Node, entering bool) ast.WalkStatus {
507 if !entering || !n.IsBlock() {
508 return ast.WalkContinue
509 }
510
511 n.RemoveIALAttr("heading-fold")
512 n.RemoveIALAttr("fold")
513 return ast.WalkContinue
514 })
515 headingChildren = headingChildren[:len(headingChildren)-1]
516 }
517 }
518
519 for i := len(headingChildren) - 1; -1 < i; i-- {
520 c := headingChildren[i]
521 targetNode.InsertAfter(c)
522 }
523 targetNode.InsertAfter(srcNode)
524 if nil != srcEmptyList {
525 srcEmptyList.Unlink()
526 }
527
528 refreshUpdated(srcNode)
529 tx.nodes[srcNode.ID] = srcNode
530 refreshUpdated(srcTree.Root)
531 if err = tx.writeTree(srcTree); err != nil {
532 return
533 }
534 if !isSameTree {
535 if err = tx.writeTree(targetTree); err != nil {
536 return
537 }
538 task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, srcTree.ID)
539 task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, srcNode.ID)
540 }
541 return
542 }
543
544 if id == targetParentID {
545 return
546 }
547
548 targetTree, err := tx.loadTree(targetParentID)
549 if err != nil {
550 logging.LogErrorf("load tree [%s] failed: %s", targetParentID, err)
551 return &TxErr{code: TxErrCodeBlockNotFound, id: targetParentID}
552 }
553 isSameTree := srcTree.ID == targetTree.ID
554 if isSameTree {
555 targetTree = srcTree
556 }
557
558 targetNode := treenode.GetNodeInTree(targetTree, targetParentID)
559 if nil == targetNode {
560 logging.LogErrorf("get node [%s] in tree [%s] failed", targetParentID, targetTree.Root.ID)
561 return &TxErr{code: TxErrCodeBlockNotFound, id: targetParentID}
562 }
563
564 if isMovingFoldHeadingIntoSelf(targetNode, headingChildren) {
565 return
566 }
567
568 if isMovingParentIntoChild(srcNode, targetNode) {
569 return
570 }
571
572 processed := false
573 if ast.NodeSuperBlock == targetNode.Type {
574 // 在布局节点后插入
575 targetNode = targetNode.FirstChild.Next
576 for i := len(headingChildren) - 1; -1 < i; i-- {
577 c := headingChildren[i]
578 targetNode.InsertAfter(c)
579 }
580 targetNode.InsertAfter(srcNode)
581 if nil != srcEmptyList {
582 srcEmptyList.Unlink()
583 }
584 processed = true
585 } else if ast.NodeListItem == targetNode.Type {
586 if 3 == targetNode.ListData.Typ {
587 // 在任务列表标记节点后插入
588 targetNode = targetNode.FirstChild
589 for i := len(headingChildren) - 1; -1 < i; i-- {
590 c := headingChildren[i]
591 targetNode.InsertAfter(c)
592 }
593 targetNode.InsertAfter(srcNode)
594 if nil != srcEmptyList {
595 srcEmptyList.Unlink()
596 }
597 processed = true
598 }
599 }
600
601 if !processed {
602 for i := len(headingChildren) - 1; -1 < i; i-- {
603 c := headingChildren[i]
604 targetNode.PrependChild(c)
605 }
606
607 targetNode.PrependChild(srcNode)
608 if nil != srcEmptyList {
609 srcEmptyList.Unlink()
610 }
611 }
612
613 refreshUpdated(srcNode)
614 tx.nodes[srcNode.ID] = srcNode
615 refreshUpdated(srcTree.Root)
616 if err = tx.writeTree(srcTree); err != nil {
617 return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
618 }
619 if !isSameTree {
620 if err = tx.writeTree(targetTree); err != nil {
621 return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
622 }
623 task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, srcTree.ID)
624 task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, srcNode.ID)
625 }
626 return
627}
628
629func isMovingFoldHeadingIntoSelf(targetNode *ast.Node, headingChildren []*ast.Node) bool {
630 for _, headingChild := range headingChildren {
631 if headingChild.ID == targetNode.ID {
632 // 不能将折叠标题移动到自己下方节点的前或后 https://github.com/siyuan-note/siyuan/issues/7163
633 return true
634 }
635 }
636 return false
637}
638
639func isMovingParentIntoChild(srcNode, targetNode *ast.Node) bool {
640 for parent := targetNode.Parent; nil != parent; parent = parent.Parent {
641 if parent.ID == srcNode.ID {
642 return true
643 }
644 }
645 return false
646}
647
648func (tx *Transaction) doPrependInsert(operation *Operation) (ret *TxErr) {
649 var err error
650 block := treenode.GetBlockTree(operation.ParentID)
651 if nil == block {
652 logging.LogWarnf("not found block [%s]", operation.ParentID)
653 util.ReloadUI() // 比如分屏后编辑器状态不一致,这里强制重新载入界面
654 return
655 }
656 tree, err := tx.loadTree(block.ID)
657 if err != nil {
658 msg := fmt.Sprintf("load tree [%s] failed: %s", block.ID, err)
659 logging.LogErrorf(msg)
660 return &TxErr{code: TxErrCodeBlockNotFound, id: block.ID}
661 }
662
663 data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
664 subTree := tx.luteEngine.BlockDOM2Tree(data)
665 insertedNode := subTree.Root.FirstChild
666 if nil == insertedNode {
667 return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: block.ID}
668 }
669 if "" == insertedNode.ID {
670 insertedNode.ID = ast.NewNodeID()
671 insertedNode.SetIALAttr("id", insertedNode.ID)
672 }
673 var toInserts []*ast.Node
674 for toInsert := insertedNode; nil != toInsert; toInsert = toInsert.Next {
675 if ast.NodeKramdownBlockIAL != toInsert.Type {
676 if "" == toInsert.ID {
677 toInsert.ID = ast.NewNodeID()
678 toInsert.SetIALAttr("id", toInsert.ID)
679 }
680 toInserts = append(toInserts, toInsert)
681 }
682 }
683
684 node := treenode.GetNodeInTree(tree, operation.ParentID)
685 if nil == node {
686 logging.LogErrorf("get node [%s] in tree [%s] failed", operation.ParentID, tree.Root.ID)
687 return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
688 }
689 isContainer := node.IsContainerBlock()
690 slices.Reverse(toInserts)
691
692 for _, toInsert := range toInserts {
693 if isContainer {
694 if ast.NodeList == node.Type {
695 // 列表下只能挂列表项,所以这里需要分情况处理
696 if ast.NodeList == toInsert.Type {
697 var childLis []*ast.Node
698 for childLi := toInsert.FirstChild; nil != childLi; childLi = childLi.Next {
699 childLis = append(childLis, childLi)
700 }
701 for i := len(childLis) - 1; -1 < i; i-- {
702 node.PrependChild(childLis[i])
703 }
704 } else {
705 newLiID := ast.NewNodeID()
706 newLi := &ast.Node{ID: newLiID, Type: ast.NodeListItem, ListData: &ast.ListData{Typ: node.ListData.Typ}}
707 newLi.SetIALAttr("id", newLiID)
708 node.PrependChild(newLi)
709 newLi.AppendChild(toInsert)
710 }
711 } else if ast.NodeSuperBlock == node.Type {
712 layout := node.ChildByType(ast.NodeSuperBlockLayoutMarker)
713 if nil != layout {
714 layout.InsertAfter(toInsert)
715 } else {
716 node.FirstChild.InsertAfter(toInsert)
717 }
718 } else {
719 node.PrependChild(toInsert)
720 }
721 } else {
722 node.InsertAfter(toInsert)
723 }
724
725 createdUpdated(toInsert)
726 tx.nodes[toInsert.ID] = toInsert
727 }
728
729 createdUpdated(insertedNode)
730 tx.nodes[insertedNode.ID] = insertedNode
731 if err = tx.writeTree(tree); err != nil {
732 return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: block.ID}
733 }
734
735 operation.ID = insertedNode.ID
736 operation.ParentID = insertedNode.Parent.ID
737
738 // 将 prependInsert 转换为 insert 推送
739 operation.Action = "insert"
740 if nil != insertedNode.Previous {
741 operation.PreviousID = insertedNode.Previous.ID
742 }
743 return
744}
745
746func (tx *Transaction) doAppendInsert(operation *Operation) (ret *TxErr) {
747 var err error
748 block := treenode.GetBlockTree(operation.ParentID)
749 if nil == block {
750 logging.LogWarnf("not found block [%s]", operation.ParentID)
751 util.ReloadUI() // 比如分屏后编辑器状态不一致,这里强制重新载入界面
752 return
753 }
754 tree, err := tx.loadTree(block.ID)
755 if err != nil {
756 msg := fmt.Sprintf("load tree [%s] failed: %s", block.ID, err)
757 logging.LogErrorf(msg)
758 return &TxErr{code: TxErrCodeBlockNotFound, id: block.ID}
759 }
760
761 data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
762 subTree := tx.luteEngine.BlockDOM2Tree(data)
763 insertedNode := subTree.Root.FirstChild
764 if nil == insertedNode {
765 return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: block.ID}
766 }
767 if "" == insertedNode.ID {
768 insertedNode.ID = ast.NewNodeID()
769 insertedNode.SetIALAttr("id", insertedNode.ID)
770 }
771 var toInserts []*ast.Node
772 for toInsert := insertedNode; nil != toInsert; toInsert = toInsert.Next {
773 if ast.NodeKramdownBlockIAL != toInsert.Type {
774 if "" == toInsert.ID {
775 toInsert.ID = ast.NewNodeID()
776 toInsert.SetIALAttr("id", toInsert.ID)
777 }
778 toInserts = append(toInserts, toInsert)
779 }
780 }
781
782 node := treenode.GetNodeInTree(tree, operation.ParentID)
783 if nil == node {
784 logging.LogErrorf("get node [%s] in tree [%s] failed", operation.ParentID, tree.Root.ID)
785 return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
786 }
787 isContainer := node.IsContainerBlock()
788 if !isContainer {
789 slices.Reverse(toInserts)
790 }
791 var lastChildBelowHeading *ast.Node
792 if ast.NodeHeading == node.Type {
793 if children := treenode.HeadingChildren(node); 0 < len(children) {
794 lastChildBelowHeading = children[len(children)-1]
795 }
796 }
797
798 for _, toInsert := range toInserts {
799 if isContainer {
800 if ast.NodeList == node.Type {
801 // 列表下只能挂列表项,所以这里需要分情况处理 https://github.com/siyuan-note/siyuan/issues/9955
802 if ast.NodeList == toInsert.Type {
803 var childLis []*ast.Node
804 for childLi := toInsert.FirstChild; nil != childLi; childLi = childLi.Next {
805 childLis = append(childLis, childLi)
806 }
807 for _, childLi := range childLis {
808 node.AppendChild(childLi)
809 }
810 } else {
811 newLiID := ast.NewNodeID()
812 newLi := &ast.Node{ID: newLiID, Type: ast.NodeListItem, ListData: &ast.ListData{Typ: node.ListData.Typ}}
813 newLi.SetIALAttr("id", newLiID)
814 node.AppendChild(newLi)
815 newLi.AppendChild(toInsert)
816 }
817 } else if ast.NodeSuperBlock == node.Type {
818 node.LastChild.InsertBefore(toInsert)
819 } else {
820 node.AppendChild(toInsert)
821 }
822 } else {
823 if ast.NodeHeading == node.Type {
824 if nil != lastChildBelowHeading {
825 lastChildBelowHeading.InsertAfter(toInsert)
826 } else {
827 node.InsertAfter(toInsert)
828 }
829 } else {
830 node.InsertAfter(toInsert)
831 }
832 }
833
834 createdUpdated(toInsert)
835 tx.nodes[toInsert.ID] = toInsert
836 }
837
838 createdUpdated(insertedNode)
839 tx.nodes[insertedNode.ID] = insertedNode
840 if err = tx.writeTree(tree); err != nil {
841 return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: block.ID}
842 }
843
844 operation.ID = insertedNode.ID
845 operation.ParentID = insertedNode.Parent.ID
846
847 // 将 appendInsert 转换为 insert 推送
848 operation.Action = "insert"
849 if nil != insertedNode.Previous {
850 operation.PreviousID = insertedNode.Previous.ID
851 }
852 return
853}
854
855func (tx *Transaction) doAppend(operation *Operation) (ret *TxErr) {
856 var err error
857 id := operation.ID
858 srcTree, err := tx.loadTree(id)
859 if err != nil {
860 logging.LogErrorf("load tree [%s] failed: %s", id, err)
861 return &TxErr{code: TxErrCodeBlockNotFound, id: id}
862 }
863
864 srcNode := treenode.GetNodeInTree(srcTree, id)
865 if nil == srcNode {
866 logging.LogErrorf("get node [%s] in tree [%s] failed", id, srcTree.Root.ID)
867 return &TxErr{code: TxErrCodeBlockNotFound, id: id}
868 }
869
870 if ast.NodeDocument == srcNode.Type {
871 logging.LogWarnf("can't append a root to another root")
872 return
873 }
874
875 var headingChildren []*ast.Node
876 if isMovingFoldHeading := ast.NodeHeading == srcNode.Type && "1" == srcNode.IALAttr("fold"); isMovingFoldHeading {
877 headingChildren = treenode.HeadingChildren(srcNode)
878 }
879 var srcEmptyList, targetNewList *ast.Node
880 if ast.NodeListItem == srcNode.Type {
881 targetNewListID := ast.NewNodeID()
882 targetNewList = &ast.Node{ID: targetNewListID, Type: ast.NodeList, ListData: &ast.ListData{Typ: srcNode.ListData.Typ}}
883 targetNewList.SetIALAttr("id", targetNewListID)
884 if srcNode.Parent.FirstChild == srcNode && srcNode.Parent.LastChild == srcNode {
885 // 列表中唯一的列表项被移除后,该列表就为空了
886 srcEmptyList = srcNode.Parent
887 }
888 }
889
890 targetRootID := operation.ParentID
891 if id == targetRootID {
892 logging.LogWarnf("target root id is nil")
893 return
894 }
895
896 targetTree, err := tx.loadTree(targetRootID)
897 if err != nil {
898 logging.LogErrorf("load tree [%s] failed: %s", targetRootID, err)
899 return &TxErr{code: TxErrCodeBlockNotFound, id: targetRootID}
900 }
901 isSameTree := srcTree.ID == targetTree.ID
902 if isSameTree {
903 targetTree = srcTree
904 }
905
906 targetRoot := targetTree.Root
907 if nil != targetNewList {
908 if nil != targetRoot.LastChild {
909 if ast.NodeList != targetRoot.LastChild.Type {
910 targetNewList.AppendChild(srcNode)
911 targetRoot.AppendChild(targetNewList)
912 } else {
913 targetRoot.LastChild.AppendChild(srcNode)
914 }
915 } else {
916 targetRoot.AppendChild(srcNode)
917 }
918 } else {
919 targetRoot.AppendChild(srcNode)
920 }
921 for _, c := range headingChildren {
922 targetRoot.AppendChild(c)
923 }
924 if nil != srcEmptyList {
925 srcEmptyList.Unlink()
926 }
927
928 if err = tx.writeTree(srcTree); err != nil {
929 return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
930 }
931
932 if !isSameTree {
933 if err = tx.writeTree(targetTree); err != nil {
934 return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
935 }
936 }
937 return
938}
939
940func (tx *Transaction) doDelete(operation *Operation) (ret *TxErr) {
941 var err error
942 id := operation.ID
943 tree, err := tx.loadTree(id)
944 if err != nil {
945 if errors.Is(err, ErrBlockNotFound) {
946 // move 以后这里会空,算作正常情况
947 return
948 }
949
950 msg := fmt.Sprintf("load tree [%s] failed: %s", id, err)
951 logging.LogErrorf(msg)
952 return &TxErr{code: TxErrCodeBlockNotFound, id: id}
953 }
954
955 node := treenode.GetNodeInTree(tree, id)
956 if nil == node {
957 return nil // move 以后的情况,列表项移动导致的状态异常 https://github.com/siyuan-note/insider/issues/961
958 }
959
960 // 收集引用的定义块 ID
961 refDefIDs := getRefDefIDs(node)
962 // 推送定义节点引用计数
963 for _, defID := range refDefIDs {
964 task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, defID)
965 }
966
967 parent := node.Parent
968 if nil != node.Next && ast.NodeKramdownBlockIAL == node.Next.Type && bytes.Contains(node.Next.Tokens, []byte(node.ID)) {
969 // 列表块撤销状态异常 https://github.com/siyuan-note/siyuan/issues/3985
970 node.Next.Unlink()
971 }
972
973 node.Unlink()
974
975 if nil != parent && ast.NodeListItem == parent.Type && nil == parent.FirstChild {
976 needAppendEmptyListItem := true
977 for _, op := range tx.DoOperations {
978 if "insert" == op.Action && op.ParentID == parent.ID {
979 needAppendEmptyListItem = false
980 break
981 }
982 }
983
984 if needAppendEmptyListItem {
985 parent.AppendChild(treenode.NewParagraph(ast.NewNodeID()))
986 }
987 }
988 treenode.RemoveBlockTree(node.ID)
989
990 delete(tx.nodes, node.ID)
991 if err = tx.writeTree(tree); err != nil {
992 return
993 }
994
995 // 如果是断开列表时的删除列表项事务,则不需要删除数据库绑定块,因为断开列表事务后面会再次插入相同 ID 的列表项
996 // List item disconnection no longer affects database binding blocks https://github.com/siyuan-note/siyuan/issues/12235
997 needSyncDel2AvBlock := true
998 if ast.NodeListItem == node.Type {
999 for _, op := range tx.DoOperations {
1000 // 不可能出现相同 ID 先插入再删除的情况,只可能出现先删除再插入的情况,所以这里只需要查找插入操作
1001 if "insert" == op.Action {
1002 data := strings.ReplaceAll(op.Data.(string), editor.FrontEndCaret, "")
1003 subTree := tx.luteEngine.BlockDOM2Tree(data)
1004 ast.Walk(subTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
1005 if !entering || ast.NodeListItem != n.Type {
1006 return ast.WalkContinue
1007 }
1008
1009 if n.ID == operation.ID {
1010 needSyncDel2AvBlock = false
1011 return ast.WalkStop
1012 }
1013 return ast.WalkContinue
1014 })
1015
1016 break
1017 }
1018 }
1019 }
1020
1021 if needSyncDel2AvBlock {
1022 syncDelete2AvBlock(node, tree, tx)
1023 }
1024 return
1025}
1026
1027func syncDelete2AvBlock(node *ast.Node, nodeTree *parse.Tree, tx *Transaction) {
1028 changedAvIDs := syncDelete2AttributeView(node)
1029 avIDs := tx.syncDelete2Block(node, nodeTree)
1030 changedAvIDs = append(changedAvIDs, avIDs...)
1031 changedAvIDs = gulu.Str.RemoveDuplicatedElem(changedAvIDs)
1032
1033 for _, avID := range changedAvIDs {
1034 ReloadAttrView(avID)
1035 }
1036}
1037
1038func (tx *Transaction) syncDelete2Block(node *ast.Node, nodeTree *parse.Tree) (changedAvIDs []string) {
1039 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
1040 if !entering || ast.NodeAttributeView != n.Type {
1041 return ast.WalkContinue
1042 }
1043
1044 avID := n.AttributeViewID
1045 isMirror := av.IsMirror(avID)
1046 if changed := av.RemoveBlockRel(avID, n.ID, treenode.ExistBlockTree); changed {
1047 changedAvIDs = append(changedAvIDs, avID)
1048 }
1049
1050 if isMirror {
1051 // 删除镜像数据库节点后不需要解绑块,因为其他镜像节点还在使用
1052 return ast.WalkContinue
1053 }
1054
1055 attrView, err := av.ParseAttributeView(avID)
1056 if err != nil {
1057 return ast.WalkContinue
1058 }
1059
1060 trees, nodes := tx.getAttrViewBoundNodes(attrView)
1061 for _, toChangNode := range nodes {
1062 avs := toChangNode.IALAttr(av.NodeAttrNameAvs)
1063 if "" != avs {
1064 avIDs := strings.Split(avs, ",")
1065 avIDs = gulu.Str.RemoveElem(avIDs, avID)
1066 if 1 > len(avIDs) {
1067 toChangNode.RemoveIALAttr(av.NodeAttrNameAvs)
1068 } else {
1069 toChangNode.SetIALAttr(av.NodeAttrNameAvs, strings.Join(avIDs, ","))
1070 }
1071 }
1072 avNames := getAvNames(toChangNode.IALAttr(av.NodeAttrNameAvs))
1073 oldAttrs := parse.IAL2Map(toChangNode.KramdownIAL)
1074 toChangNode.SetIALAttr(av.NodeAttrViewNames, avNames)
1075 pushBroadcastAttrTransactions(oldAttrs, toChangNode)
1076 }
1077
1078 for _, tree := range trees {
1079 if nodeTree.ID != tree.ID {
1080 indexWriteTreeUpsertQueue(tree)
1081 }
1082 }
1083 return ast.WalkContinue
1084 })
1085
1086 changedAvIDs = gulu.Str.RemoveDuplicatedElem(changedAvIDs)
1087 return
1088}
1089
1090func syncDelete2AttributeView(node *ast.Node) (changedAvIDs []string) {
1091 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
1092 if !entering || !n.IsBlock() {
1093 return ast.WalkContinue
1094 }
1095
1096 avs := n.IALAttr(av.NodeAttrNameAvs)
1097 if "" == avs {
1098 return ast.WalkContinue
1099 }
1100
1101 avIDs := strings.Split(avs, ",")
1102 for _, avID := range avIDs {
1103 attrView, parseErr := av.ParseAttributeView(avID)
1104 if nil != parseErr {
1105 continue
1106 }
1107
1108 changedAv := false
1109 blockValues := attrView.GetBlockKeyValues()
1110 if nil == blockValues {
1111 continue
1112 }
1113
1114 for i, blockValue := range blockValues.Values {
1115 if nil == blockValue.Block {
1116 continue
1117 }
1118
1119 if blockValue.Block.ID == n.ID {
1120 blockValues.Values = append(blockValues.Values[:i], blockValues.Values[i+1:]...)
1121 changedAv = true
1122 break
1123 }
1124 }
1125
1126 if changedAv {
1127 regenAttrViewGroups(attrView)
1128 av.SaveAttributeView(attrView)
1129 changedAvIDs = append(changedAvIDs, avID)
1130 }
1131 }
1132 return ast.WalkContinue
1133 })
1134
1135 changedAvIDs = gulu.Str.RemoveDuplicatedElem(changedAvIDs)
1136 return
1137}
1138
1139func (tx *Transaction) doLargeInsert(previousID string) (ret *TxErr) {
1140 tree, err := tx.loadTree(previousID)
1141 if nil != err {
1142 logging.LogErrorf("load tree [%s] failed: %s", previousID, err)
1143 return &TxErr{code: TxErrCodeBlockNotFound, id: previousID}
1144 }
1145
1146 for _, operation := range tx.DoOperations {
1147 if "insert" != operation.Action {
1148 break
1149 }
1150
1151 data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
1152 subTree := tx.luteEngine.BlockDOM2Tree(data)
1153 tx.processGlobalAssets(subTree)
1154
1155 insertedNode := subTree.Root.FirstChild
1156 if nil == insertedNode {
1157 return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: tree.ID}
1158 }
1159 var remains []*ast.Node
1160 for remain := insertedNode.Next; nil != remain; remain = remain.Next {
1161 if ast.NodeKramdownBlockIAL != remain.Type {
1162 if "" == remain.ID {
1163 remain.ID = ast.NewNodeID()
1164 remain.SetIALAttr("id", remain.ID)
1165 }
1166 remains = append(remains, remain)
1167 }
1168 }
1169 if "" == insertedNode.ID {
1170 insertedNode.ID = ast.NewNodeID()
1171 insertedNode.SetIALAttr("id", insertedNode.ID)
1172 }
1173
1174 node := treenode.GetNodeInTree(tree, previousID)
1175 if nil == node {
1176 logging.LogErrorf("get node [%s] in tree [%s] failed", previousID, tree.Root.ID)
1177 return &TxErr{code: TxErrCodeBlockNotFound, id: previousID}
1178 }
1179
1180 if ast.NodeHeading == node.Type && "1" == node.IALAttr("fold") {
1181 children := treenode.HeadingChildren(node)
1182 if l := len(children); 0 < l {
1183 node = children[l-1]
1184 }
1185 }
1186 if ast.NodeList == insertedNode.Type && nil != node.Parent && ast.NodeList == node.Parent.Type {
1187 insertedNode = insertedNode.FirstChild
1188 }
1189 for i := len(remains) - 1; 0 <= i; i-- {
1190 remain := remains[i]
1191 node.InsertAfter(remain)
1192 }
1193 node.InsertAfter(insertedNode)
1194
1195 createdUpdated(insertedNode)
1196 tx.nodes[insertedNode.ID] = insertedNode
1197 tx.trees[tree.ID] = tree
1198
1199 // 收集引用的定义块 ID
1200 refDefIDs := getRefDefIDs(insertedNode)
1201 // 推送定义节点引用计数
1202 for _, defID := range refDefIDs {
1203 task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, defID)
1204 }
1205
1206 upsertAvBlockRel(insertedNode)
1207
1208 // 复制为副本时将该副本块插入到数据库中 https://github.com/siyuan-note/siyuan/issues/11959
1209 avs := insertedNode.IALAttr(av.NodeAttrNameAvs)
1210 for _, avID := range strings.Split(avs, ",") {
1211 if !ast.IsNodeIDPattern(avID) {
1212 continue
1213 }
1214
1215 AddAttributeViewBlock(tx, []map[string]interface{}{{
1216 "id": insertedNode.ID,
1217 "isDetached": false,
1218 }}, avID, "", "", "", previousID, false, map[string]interface{}{})
1219 ReloadAttrView(avID)
1220 }
1221
1222 if ast.NodeAttributeView == insertedNode.Type {
1223 // 插入数据库块时需要重新绑定其中已经存在的块
1224 // 比如剪切操作时,会先进行 delete 数据库解绑块,这里需要重新绑定 https://github.com/siyuan-note/siyuan/issues/13031
1225 attrView, parseErr := av.ParseAttributeView(insertedNode.AttributeViewID)
1226 if nil == parseErr {
1227 trees, toBindNodes := tx.getAttrViewBoundNodes(attrView)
1228 for _, toBindNode := range toBindNodes {
1229 t := trees[toBindNode.ID]
1230 bindBlockAv0(tx, insertedNode.AttributeViewID, toBindNode, t)
1231 }
1232
1233 // 设置视图 https://github.com/siyuan-note/siyuan/issues/15279
1234 v := attrView.GetView(attrView.ViewID)
1235 if nil != v {
1236 insertedNode.AttributeViewType = string(v.LayoutType)
1237 attrs := parse.IAL2Map(insertedNode.KramdownIAL)
1238 if "" == attrs[av.NodeAttrView] {
1239 attrs[av.NodeAttrView] = v.ID
1240 err = setNodeAttrs(insertedNode, tree, attrs)
1241 if err != nil {
1242 logging.LogWarnf("set node [%s] attrs failed: %s", operation.BlockID, err)
1243 return
1244 }
1245 }
1246 }
1247 }
1248 }
1249
1250 operation.ID = insertedNode.ID
1251 operation.ParentID = insertedNode.Parent.ID
1252 }
1253
1254 if err = tx.writeTree(tree); nil != err {
1255 return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: tree.ID}
1256 }
1257 return tx.doDelete(tx.DoOperations[len(tx.DoOperations)-1])
1258}
1259
1260func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) {
1261 var bt *treenode.BlockTree
1262 bts := treenode.GetBlockTrees([]string{operation.ParentID, operation.PreviousID, operation.NextID})
1263 for _, b := range bts {
1264 if "" != b.ID {
1265 bt = b
1266 break
1267 }
1268 }
1269 if nil == bt {
1270 logging.LogWarnf("not found block tree [%s, %s, %s]", operation.ParentID, operation.PreviousID, operation.NextID)
1271 util.ReloadUI() // 比如分屏后编辑器状态不一致,这里强制重新载入界面
1272 return
1273 }
1274
1275 var err error
1276 tree, err := tx.loadTreeByBlockTree(bt)
1277 if err != nil {
1278 msg := fmt.Sprintf("load tree [%s] failed: %s", bt.ID, err)
1279 logging.LogErrorf(msg)
1280 return &TxErr{code: TxErrCodeBlockNotFound, id: bt.ID}
1281 }
1282
1283 data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
1284 subTree := tx.luteEngine.BlockDOM2Tree(data)
1285 tx.processGlobalAssets(subTree)
1286
1287 insertedNode := subTree.Root.FirstChild
1288 if nil == insertedNode {
1289 return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: bt.ID}
1290 }
1291 var remains []*ast.Node
1292 for remain := insertedNode.Next; nil != remain; remain = remain.Next {
1293 if ast.NodeKramdownBlockIAL != remain.Type {
1294 if "" == remain.ID {
1295 remain.ID = ast.NewNodeID()
1296 remain.SetIALAttr("id", remain.ID)
1297 }
1298 remains = append(remains, remain)
1299 }
1300 }
1301 if "" == insertedNode.ID {
1302 insertedNode.ID = ast.NewNodeID()
1303 insertedNode.SetIALAttr("id", insertedNode.ID)
1304 }
1305
1306 var node *ast.Node
1307 nextID := operation.NextID
1308 previousID := operation.PreviousID
1309 if "" != nextID {
1310 node = treenode.GetNodeInTree(tree, nextID)
1311 if nil == node {
1312 logging.LogErrorf("get node [%s] in tree [%s] failed", nextID, tree.Root.ID)
1313 return &TxErr{code: TxErrCodeBlockNotFound, id: nextID}
1314 }
1315
1316 if ast.NodeList == insertedNode.Type && nil != node.Parent && ast.NodeList == node.Parent.Type {
1317 insertedNode = insertedNode.FirstChild
1318 }
1319 node.InsertBefore(insertedNode)
1320 } else if "" != previousID {
1321 node = treenode.GetNodeInTree(tree, previousID)
1322 if nil == node {
1323 logging.LogErrorf("get node [%s] in tree [%s] failed", previousID, tree.Root.ID)
1324 return &TxErr{code: TxErrCodeBlockNotFound, id: previousID}
1325 }
1326
1327 if ast.NodeHeading == node.Type && "1" == node.IALAttr("fold") {
1328 children := treenode.HeadingChildren(node)
1329 if l := len(children); 0 < l {
1330 node = children[l-1]
1331 }
1332 }
1333 if ast.NodeList == insertedNode.Type && nil != node.Parent && ast.NodeList == node.Parent.Type {
1334 insertedNode = insertedNode.FirstChild
1335 }
1336 for i := len(remains) - 1; 0 <= i; i-- {
1337 remain := remains[i]
1338 node.InsertAfter(remain)
1339 }
1340 node.InsertAfter(insertedNode)
1341 } else {
1342 node = treenode.GetNodeInTree(tree, operation.ParentID)
1343 if nil == node {
1344 logging.LogErrorf("get node [%s] in tree [%s] failed", operation.ParentID, tree.Root.ID)
1345 return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
1346 }
1347 if ast.NodeSuperBlock == node.Type {
1348 // 在布局节点后插入
1349 node.FirstChild.Next.InsertAfter(insertedNode)
1350 } else {
1351 if ast.NodeList == insertedNode.Type && nil != insertedNode.FirstChild && operation.ID == insertedNode.FirstChild.ID && operation.ID != insertedNode.ID {
1352 // 将一个列表项移动到另一个列表的第一项时 https://github.com/siyuan-note/siyuan/issues/2341
1353 insertedNode = insertedNode.FirstChild
1354 }
1355
1356 if ast.NodeListItem == node.Type && 3 == node.ListData.Typ {
1357 // 在任务列表标记节点后插入
1358 node.FirstChild.InsertAfter(insertedNode)
1359 for _, remain := range remains {
1360 node.FirstChild.InsertAfter(remain)
1361 }
1362 } else {
1363 if !node.IsContainerBlock() {
1364 for i := len(remains) - 1; 0 <= i; i-- {
1365 remain := remains[i]
1366 node.InsertAfter(remain)
1367 }
1368 node.InsertAfter(insertedNode)
1369 } else {
1370 for i := len(remains) - 1; 0 <= i; i-- {
1371 remain := remains[i]
1372 node.PrependChild(remain)
1373 }
1374 node.PrependChild(insertedNode)
1375 }
1376 }
1377 }
1378 }
1379
1380 createdUpdated(insertedNode)
1381 tx.nodes[insertedNode.ID] = insertedNode
1382 if err = tx.writeTree(tree); err != nil {
1383 return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: bt.ID}
1384 }
1385
1386 // 收集引用的定义块 ID
1387 refDefIDs := getRefDefIDs(insertedNode)
1388 // 推送定义节点引用计数
1389 for _, defID := range refDefIDs {
1390 task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, defID)
1391 }
1392
1393 upsertAvBlockRel(insertedNode)
1394
1395 // 复制为副本时将该副本块插入到数据库中 https://github.com/siyuan-note/siyuan/issues/11959
1396 avs := insertedNode.IALAttr(av.NodeAttrNameAvs)
1397 for _, avID := range strings.Split(avs, ",") {
1398 if !ast.IsNodeIDPattern(avID) {
1399 continue
1400 }
1401
1402 AddAttributeViewBlock(tx, []map[string]interface{}{{
1403 "id": insertedNode.ID,
1404 "isDetached": false,
1405 }}, avID, "", "", "", previousID, false, map[string]interface{}{})
1406 ReloadAttrView(avID)
1407 }
1408
1409 if ast.NodeAttributeView == insertedNode.Type {
1410 // 插入数据库块时需要重新绑定其中已经存在的块
1411 // 比如剪切操作时,会先进行 delete 数据库解绑块,这里需要重新绑定 https://github.com/siyuan-note/siyuan/issues/13031
1412 attrView, parseErr := av.ParseAttributeView(insertedNode.AttributeViewID)
1413 if nil == parseErr {
1414 trees, toBindNodes := tx.getAttrViewBoundNodes(attrView)
1415 for _, toBindNode := range toBindNodes {
1416 t := trees[toBindNode.ID]
1417 bindBlockAv0(tx, insertedNode.AttributeViewID, toBindNode, t)
1418 }
1419
1420 // 设置视图 https://github.com/siyuan-note/siyuan/issues/15279
1421 v := attrView.GetView(attrView.ViewID)
1422 if nil != v {
1423 insertedNode.AttributeViewType = string(v.LayoutType)
1424 attrs := parse.IAL2Map(insertedNode.KramdownIAL)
1425 if "" == attrs[av.NodeAttrView] {
1426 attrs[av.NodeAttrView] = v.ID
1427 err = setNodeAttrs(insertedNode, tree, attrs)
1428 if err != nil {
1429 logging.LogWarnf("set node [%s] attrs failed: %s", operation.BlockID, err)
1430 return
1431 }
1432 }
1433 }
1434 }
1435 }
1436
1437 operation.ID = insertedNode.ID
1438 operation.ParentID = insertedNode.Parent.ID
1439 return
1440}
1441
1442func (tx *Transaction) processGlobalAssets(tree *parse.Tree) {
1443 if !tx.isGlobalAssetsInit {
1444 tx.assetsDir = getAssetsDir(filepath.Join(util.DataDir, tree.Box), filepath.Dir(filepath.Join(util.DataDir, tree.Box, tree.Path)))
1445 tx.isGlobalAssets = strings.HasPrefix(tx.assetsDir, filepath.Join(util.DataDir, "assets"))
1446 tx.isGlobalAssetsInit = true
1447 }
1448
1449 if tx.isGlobalAssets {
1450 return
1451 }
1452
1453 // 本地资源文件需要移动到用户手动建立的 assets 下 https://github.com/siyuan-note/siyuan/issues/2410
1454 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
1455 if !entering {
1456 return ast.WalkContinue
1457 }
1458
1459 if ast.NodeLinkDest == n.Type && bytes.HasPrefix(n.Tokens, []byte("assets/")) {
1460 assetP := gulu.Str.FromBytes(n.Tokens)
1461 assetPath, e := GetAssetAbsPath(assetP)
1462 if nil != e {
1463 logging.LogErrorf("get path of asset [%s] failed: %s", assetP, e)
1464 return ast.WalkContinue
1465 }
1466
1467 if !strings.HasPrefix(assetPath, filepath.Join(util.DataDir, "assets")) {
1468 // 非全局 assets 则跳过
1469 return ast.WalkContinue
1470 }
1471
1472 // 只有全局 assets 才移动到相对 assets
1473 targetP := filepath.Join(tx.assetsDir, filepath.Base(assetPath))
1474 if e = filelock.Rename(assetPath, targetP); e != nil {
1475 logging.LogErrorf("copy path of asset from [%s] to [%s] failed: %s", assetPath, targetP, e)
1476 return ast.WalkContinue
1477 }
1478 }
1479 return ast.WalkContinue
1480 })
1481}
1482
1483func (tx *Transaction) doUpdate(operation *Operation) (ret *TxErr) {
1484 id := operation.ID
1485 tree, err := tx.loadTree(id)
1486 if err != nil {
1487 logging.LogErrorf("load tree [%s] failed: %s", id, err)
1488 return &TxErr{code: TxErrCodeBlockNotFound, id: id}
1489 }
1490
1491 data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
1492 if "" == data {
1493 logging.LogErrorf("update data is nil")
1494 return &TxErr{code: TxErrCodeBlockNotFound, id: id}
1495 }
1496
1497 subTree := tx.luteEngine.BlockDOM2Tree(data)
1498 subTree.ID, subTree.Box, subTree.Path = tree.ID, tree.Box, tree.Path
1499 oldNode := treenode.GetNodeInTree(tree, id)
1500 if nil == oldNode {
1501 logging.LogErrorf("get node [%s] in tree [%s] failed", id, tree.Root.ID)
1502 return &TxErr{msg: ErrBlockNotFound.Error(), id: id}
1503 }
1504
1505 // 收集引用的定义块 ID
1506 oldDefIDs := getRefDefIDs(oldNode)
1507 var newDefIDs []string
1508
1509 var unlinks []*ast.Node
1510 ast.Walk(subTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
1511 if !entering {
1512 return ast.WalkContinue
1513 }
1514
1515 if ast.NodeTextMark == n.Type {
1516 if n.IsTextMarkType("inline-math") {
1517 if "" == strings.TrimSpace(n.TextMarkInlineMathContent) {
1518 // 剔除空白的行级公式
1519 unlinks = append(unlinks, n)
1520 }
1521 } else if n.IsTextMarkType("block-ref") {
1522 sql.CacheRef(subTree, n)
1523
1524 if "d" == n.TextMarkBlockRefSubtype {
1525 // 偶发编辑文档标题后引用处的动态锚文本不更新 https://github.com/siyuan-note/siyuan/issues/5891
1526 // 使用缓存的动态锚文本强制覆盖当前块中的引用节点动态锚文本
1527 if dRefText, ok := treenode.DynamicRefTexts.Load(n.TextMarkBlockRefID); ok && "" != dRefText {
1528 n.TextMarkTextContent = dRefText.(string)
1529 }
1530 }
1531
1532 newDefIDs = append(newDefIDs, n.TextMarkBlockRefID)
1533 }
1534 }
1535 return ast.WalkContinue
1536 })
1537 for _, n := range unlinks {
1538 n.Unlink()
1539 }
1540
1541 oldDefIDs = gulu.Str.RemoveDuplicatedElem(oldDefIDs)
1542 newDefIDs = gulu.Str.RemoveDuplicatedElem(newDefIDs)
1543 refDefIDs := oldDefIDs
1544
1545 if !slices.Equal(oldDefIDs, newDefIDs) { // 如果引用发生了变化,则推送定义节点引用计数
1546 refDefIDs = append(refDefIDs, newDefIDs...)
1547 refDefIDs = gulu.Str.RemoveDuplicatedElem(refDefIDs)
1548 for _, defID := range refDefIDs {
1549 task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, defID)
1550 }
1551 }
1552
1553 updatedNode := subTree.Root.FirstChild
1554 if nil == updatedNode {
1555 logging.LogErrorf("get fist node in sub tree [%s] failed", subTree.Root.ID)
1556 return &TxErr{msg: ErrBlockNotFound.Error(), id: id}
1557 }
1558 if ast.NodeList == updatedNode.Type && ast.NodeList == oldNode.Parent.Type {
1559 updatedNode = updatedNode.FirstChild
1560 }
1561
1562 if oldNode.IsContainerBlock() {
1563 // 更新容器块的话需要考虑其子块中可能存在的折叠标题,需要把这些折叠标题的下方块移动到新节点下面
1564 treenode.MoveFoldHeading(updatedNode, oldNode)
1565 }
1566
1567 cache.PutBlockIAL(updatedNode.ID, parse.IAL2Map(updatedNode.KramdownIAL))
1568
1569 if ast.NodeHTMLBlock == updatedNode.Type {
1570 content := string(updatedNode.Tokens)
1571 // 剔除连续的空行(包括空行内包含空格的情况) https://github.com/siyuan-note/siyuan/issues/15377
1572 var newLines []string
1573 lines := strings.Split(content, "\n")
1574 for _, line := range lines {
1575 if strings.TrimSpace(line) != "" {
1576 newLines = append(newLines, line)
1577 }
1578 }
1579 updatedNode.Tokens = []byte(strings.Join(newLines, "\n"))
1580 }
1581
1582 removedNodes := getRemovedNodes(oldNode, updatedNode)
1583 for _, n := range removedNodes {
1584 syncDelete2AvBlock(n, tree, tx)
1585 }
1586
1587 // 将不属于折叠标题的块移动到折叠标题下方,需要展开折叠标题
1588 needUnfoldParentHeading := 0 < oldNode.HeadingLevel && (0 == updatedNode.HeadingLevel || oldNode.HeadingLevel < updatedNode.HeadingLevel)
1589
1590 oldParentFoldedHeading := treenode.GetParentFoldedHeading(oldNode)
1591 // 将原先折叠标题下的块提升为与折叠标题同级或更高一级的标题时,需要在折叠标题后插入该提升后的标题块(只需要推送界面插入)
1592 needInsertAfterParentHeading := nil != oldParentFoldedHeading && 0 != updatedNode.HeadingLevel && updatedNode.HeadingLevel <= oldParentFoldedHeading.HeadingLevel
1593
1594 oldNode.InsertAfter(updatedNode)
1595 oldNode.Unlink()
1596
1597 if needUnfoldParentHeading {
1598 newParentFoldedHeading := treenode.GetParentFoldedHeading(updatedNode)
1599 if nil == oldParentFoldedHeading || (nil != newParentFoldedHeading && oldParentFoldedHeading.ID != newParentFoldedHeading.ID) {
1600 unfoldHeading(newParentFoldedHeading, updatedNode)
1601 }
1602 }
1603
1604 if needInsertAfterParentHeading {
1605 insertDom := data
1606 if 2 == len(tx.DoOperations) && "foldHeading" == tx.DoOperations[1].Action {
1607 children := treenode.HeadingChildren(updatedNode)
1608 for _, child := range children {
1609 ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus {
1610 if !entering || !n.IsBlock() {
1611 return ast.WalkContinue
1612 }
1613
1614 n.SetIALAttr("fold", "1")
1615 n.SetIALAttr("heading-fold", "1")
1616 return ast.WalkContinue
1617 })
1618 }
1619 updatedNode.SetIALAttr("fold", "1")
1620 insertDom = tx.luteEngine.RenderNodeBlockDOM(updatedNode)
1621 }
1622
1623 evt := util.NewCmdResult("transactions", 0, util.PushModeBroadcast)
1624 evt.Data = []*Transaction{{
1625 DoOperations: []*Operation{{Action: "insert", ID: updatedNode.ID, PreviousID: oldParentFoldedHeading.ID, Data: insertDom}},
1626 UndoOperations: []*Operation{{Action: "delete", ID: updatedNode.ID}},
1627 }}
1628 util.PushEvent(evt)
1629 }
1630
1631 createdUpdated(updatedNode)
1632 tx.nodes[updatedNode.ID] = updatedNode
1633 if err = tx.writeTree(tree); err != nil {
1634 return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
1635 }
1636
1637 upsertAvBlockRel(updatedNode)
1638
1639 if ast.NodeAttributeView == updatedNode.Type {
1640 // 设置视图 https://github.com/siyuan-note/siyuan/issues/15279
1641 attrView, parseErr := av.ParseAttributeView(updatedNode.AttributeViewID)
1642 if nil == parseErr {
1643 v := attrView.GetView(attrView.ViewID)
1644 if nil != v {
1645 updatedNode.AttributeViewType = string(v.LayoutType)
1646 attrs := parse.IAL2Map(updatedNode.KramdownIAL)
1647 if "" == attrs[av.NodeAttrView] {
1648 attrs[av.NodeAttrView] = v.ID
1649 err = setNodeAttrs(updatedNode, tree, attrs)
1650 if err != nil {
1651 logging.LogWarnf("set node [%s] attrs failed: %s", operation.BlockID, err)
1652 return &TxErr{code: TxErrCodeBlockNotFound, id: id}
1653 }
1654 }
1655 }
1656 }
1657 }
1658 return
1659}
1660
1661func unfoldHeading(heading, currentNode *ast.Node) {
1662 if nil == heading {
1663 return
1664 }
1665
1666 children := treenode.HeadingChildren(heading)
1667 for _, child := range children {
1668 ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus {
1669 if !entering || !n.IsBlock() {
1670 return ast.WalkContinue
1671 }
1672
1673 n.RemoveIALAttr("fold")
1674 n.RemoveIALAttr("heading-fold")
1675 return ast.WalkContinue
1676 })
1677 }
1678 heading.RemoveIALAttr("fold")
1679 heading.RemoveIALAttr("heading-fold")
1680
1681 util.BroadcastByType("protyle", "unfoldHeading", 0, "", map[string]interface{}{"id": heading.ID, "currentNodeID": currentNode.ID})
1682}
1683
1684func getRefDefIDs(node *ast.Node) (refDefIDs []string) {
1685 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
1686 if !entering {
1687 return ast.WalkContinue
1688 }
1689
1690 if treenode.IsBlockRef(n) {
1691 refDefIDs = append(refDefIDs, n.TextMarkBlockRefID)
1692 } else if treenode.IsEmbedBlockRef(n) {
1693 defID := treenode.GetEmbedBlockRef(n)
1694 refDefIDs = append(refDefIDs, defID)
1695 }
1696 return ast.WalkContinue
1697 })
1698 refDefIDs = gulu.Str.RemoveDuplicatedElem(refDefIDs)
1699 return
1700}
1701
1702func getRemovedNodes(oldNode, newNode *ast.Node) (ret []*ast.Node) {
1703 oldNodes := map[string]*ast.Node{}
1704 ast.Walk(oldNode, func(n *ast.Node, entering bool) ast.WalkStatus {
1705 if !entering || !n.IsBlock() {
1706 return ast.WalkContinue
1707 }
1708 oldNodes[n.ID] = n
1709 return ast.WalkContinue
1710 })
1711 ast.Walk(newNode, func(n *ast.Node, entering bool) ast.WalkStatus {
1712 if !entering || !n.IsBlock() {
1713 return ast.WalkContinue
1714 }
1715 if _, ok := oldNodes[n.ID]; ok {
1716 delete(oldNodes, n.ID)
1717 }
1718 return ast.WalkContinue
1719 })
1720 for _, n := range oldNodes {
1721 ret = append(ret, n)
1722 }
1723 return
1724}
1725
1726func upsertAvBlockRel(node *ast.Node) {
1727 var affectedAvIDs []string
1728 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
1729 if !entering {
1730 return ast.WalkContinue
1731 }
1732
1733 if ast.NodeAttributeView == n.Type {
1734 avID := n.AttributeViewID
1735 if changed := av.UpsertBlockRel(avID, n.ID); changed {
1736 affectedAvIDs = append(affectedAvIDs, avID)
1737 }
1738 }
1739 return ast.WalkContinue
1740 })
1741
1742 updatedNodes := []*ast.Node{node}
1743 var parents []*ast.Node
1744 for parent := node.Parent; nil != parent && ast.NodeDocument != parent.Type; parent = parent.Parent {
1745 parents = append(parents, parent)
1746 }
1747 updatedNodes = append(updatedNodes, parents...)
1748 for _, updatedNode := range updatedNodes {
1749 ast.Walk(updatedNode, func(n *ast.Node, entering bool) ast.WalkStatus {
1750 avs := n.IALAttr(av.NodeAttrNameAvs)
1751 if "" == avs {
1752 return ast.WalkContinue
1753 }
1754
1755 avIDs := strings.Split(avs, ",")
1756 affectedAvIDs = append(affectedAvIDs, avIDs...)
1757 return ast.WalkContinue
1758 })
1759 }
1760
1761 go func() {
1762 time.Sleep(100 * time.Millisecond)
1763 sql.FlushQueue()
1764
1765 affectedAvIDs = gulu.Str.RemoveDuplicatedElem(affectedAvIDs)
1766 var relatedAvIDs []string
1767 for _, avID := range affectedAvIDs {
1768 relatedAvIDs = append(relatedAvIDs, av.GetSrcAvIDs(avID)...)
1769 }
1770 affectedAvIDs = append(affectedAvIDs, relatedAvIDs...)
1771 affectedAvIDs = gulu.Str.RemoveDuplicatedElem(affectedAvIDs)
1772 for _, avID := range affectedAvIDs {
1773 attrView, _ := av.ParseAttributeView(avID)
1774 if nil != attrView {
1775 regenAttrViewGroups(attrView)
1776 av.SaveAttributeView(attrView)
1777 }
1778
1779 ReloadAttrView(avID)
1780 }
1781 }()
1782}
1783
1784func (tx *Transaction) doUpdateUpdated(operation *Operation) (ret *TxErr) {
1785 id := operation.ID
1786 tree, err := tx.loadTree(id)
1787 if err != nil {
1788 if errors.Is(err, ErrBlockNotFound) {
1789 logging.LogWarnf("not found block [%s]", id)
1790 return
1791 }
1792
1793 logging.LogErrorf("load tree [%s] failed: %s", id, err)
1794 return &TxErr{code: TxErrCodeBlockNotFound, id: id}
1795 }
1796
1797 node := treenode.GetNodeInTree(tree, id)
1798 if nil == node {
1799 logging.LogErrorf("get node [%s] in tree [%s] failed", id, tree.Root.ID)
1800 return &TxErr{msg: ErrBlockNotFound.Error(), id: id}
1801 }
1802
1803 node.SetIALAttr("updated", operation.Data.(string))
1804 createdUpdated(node)
1805 tx.nodes[node.ID] = node
1806 if err = tx.writeTree(tree); err != nil {
1807 return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
1808 }
1809 return
1810}
1811
1812func (tx *Transaction) doCreate(operation *Operation) (ret *TxErr) {
1813 tree := operation.Data.(*parse.Tree)
1814 tx.writeTree(tree)
1815 return
1816}
1817
1818func (tx *Transaction) doSetAttrs(operation *Operation) (ret *TxErr) {
1819 id := operation.ID
1820 tree, err := tx.loadTree(id)
1821 if err != nil {
1822 logging.LogErrorf("load tree [%s] failed: %s", id, err)
1823 return &TxErr{code: TxErrCodeBlockNotFound, id: id}
1824 }
1825
1826 node := treenode.GetNodeInTree(tree, id)
1827 if nil == node {
1828 logging.LogErrorf("get node [%s] in tree [%s] failed", id, tree.Root.ID)
1829 return &TxErr{code: TxErrCodeBlockNotFound, id: id}
1830 }
1831
1832 attrs := map[string]string{}
1833 if err = gulu.JSON.UnmarshalJSON([]byte(operation.Data.(string)), &attrs); err != nil {
1834 logging.LogErrorf("unmarshal attrs failed: %s", err)
1835 return &TxErr{code: TxErrCodeBlockNotFound, id: id}
1836 }
1837
1838 var invalidNames []string
1839 for name := range attrs {
1840 for i := 0; i < len(name); i++ {
1841 if !lex.IsASCIILetterNumHyphen(name[i]) {
1842 logging.LogWarnf("invalid attr name [%s]", name)
1843 invalidNames = append(invalidNames, name)
1844 }
1845 }
1846 }
1847 for _, name := range invalidNames {
1848 delete(attrs, name)
1849 }
1850
1851 for name, value := range attrs {
1852 if "" == value {
1853 node.RemoveIALAttr(name)
1854 } else {
1855 node.SetIALAttr(name, value)
1856 }
1857 }
1858
1859 if err = tx.writeTree(tree); err != nil {
1860 return
1861 }
1862 cache.PutBlockIAL(id, parse.IAL2Map(node.KramdownIAL))
1863 return
1864}
1865
1866func refreshUpdated(node *ast.Node) {
1867 updated := util.CurrentTimeSecondsStr()
1868 node.SetIALAttr("updated", updated)
1869 parents := treenode.ParentNodesWithHeadings(node)
1870 for _, parent := range parents { // 更新所有父节点的更新时间字段
1871 parent.SetIALAttr("updated", updated)
1872 }
1873}
1874
1875func createdUpdated(node *ast.Node) {
1876 // 补全子节点的更新时间 Improve block update time filling https://github.com/siyuan-note/siyuan/issues/12182
1877 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
1878 if !entering || !n.IsBlock() || ast.NodeKramdownBlockIAL == n.Type {
1879 return ast.WalkContinue
1880 }
1881
1882 updated := n.IALAttr("updated")
1883 if "" == updated && ast.IsNodeIDPattern(n.ID) {
1884 created := util.TimeFromID(n.ID)
1885 n.SetIALAttr("updated", created)
1886 }
1887 return ast.WalkContinue
1888 })
1889
1890 created := util.TimeFromID(node.ID)
1891 updated := node.IALAttr("updated")
1892 if "" == updated {
1893 updated = created
1894 }
1895 if updated < created {
1896 updated = created
1897 }
1898 parents := treenode.ParentNodesWithHeadings(node)
1899 for _, parent := range parents { // 更新所有父节点的更新时间字段
1900 parent.SetIALAttr("updated", updated)
1901 cache.PutBlockIAL(parent.ID, parse.IAL2Map(parent.KramdownIAL))
1902 }
1903}
1904
1905type Operation struct {
1906 Action string `json:"action"`
1907 Data interface{} `json:"data"`
1908 ID string `json:"id"`
1909 ParentID string `json:"parentID"`
1910 PreviousID string `json:"previousID"`
1911 NextID string `json:"nextID"`
1912 RetData interface{} `json:"retData"`
1913 BlockIDs []string `json:"blockIDs"`
1914 BlockID string `json:"blockID"`
1915
1916 DeckID string `json:"deckID"` // 用于添加/删除闪卡
1917
1918 AvID string `json:"avID"` // 属性视图 ID
1919 SrcIDs []string `json:"srcIDs"` // 用于从属性视图中删除行
1920 Srcs []map[string]interface{} `json:"srcs"` // 用于添加属性视图行(包括绑定块){id, content, isDetached}
1921 IsDetached bool `json:"isDetached"` // 用于标识是否未绑定块,仅存在于属性视图中
1922 Name string `json:"name"` // 属性视图列名
1923 Typ string `json:"type"` // 属性视图列类型
1924 Format string `json:"format"` // 属性视图列格式化
1925 KeyID string `json:"keyID"` // 属性视图字段 ID
1926 RowID string `json:"rowID"` // 属性视图行 ID
1927 IsTwoWay bool `json:"isTwoWay"` // 属性视图关联列是否是双向关系
1928 BackRelationKeyID string `json:"backRelationKeyID"` // 属性视图关联列回链关联列的 ID
1929 RemoveDest bool `json:"removeDest"` // 属性视图删除关联目标
1930 Layout av.LayoutType `json:"layout"` // 属性视图布局类型
1931 GroupID string `json:"groupID"` // 属性视图分组视图 ID
1932 TargetGroupID string `json:"targetGroupID"` // 属性视图目标分组视图 ID
1933 ViewID string `json:"viewID"` // 属性视图视图 ID
1934 IgnoreDefaultFill bool `json:"ignoreDefaultFill"` // 是否忽略默认填充
1935
1936 Context map[string]interface{} `json:"context"` // 上下文信息
1937}
1938
1939type Transaction struct {
1940 Timestamp int64 `json:"timestamp"`
1941 DoOperations []*Operation `json:"doOperations"`
1942 UndoOperations []*Operation `json:"undoOperations"`
1943
1944 trees map[string]*parse.Tree // 事务中变更的树
1945 nodes map[string]*ast.Node // 事务中变更的节点
1946
1947 isGlobalAssetsInit bool // 是否初始化过全局资源判断
1948 isGlobalAssets bool // 是否属于全局资源
1949 assetsDir string // 资源目录路径
1950
1951 luteEngine *lute.Lute
1952 m *sync.Mutex
1953 state atomic.Int32 // 0: 初始化,1:未提交,:2: 已提交,3: 已回滚
1954}
1955
1956func (tx *Transaction) WaitForCommit() {
1957 for {
1958 if 1 == tx.state.Load() {
1959 time.Sleep(10 * time.Millisecond)
1960 continue
1961 }
1962 return
1963 }
1964}
1965
1966func (tx *Transaction) begin() (err error) {
1967 tx.trees = map[string]*parse.Tree{}
1968 tx.nodes = map[string]*ast.Node{}
1969 tx.luteEngine = util.NewLute()
1970 tx.m.Lock()
1971 tx.state.Store(1)
1972 return
1973}
1974
1975func (tx *Transaction) commit() (err error) {
1976 for _, tree := range tx.trees {
1977 if err = writeTreeUpsertQueue(tree); err != nil {
1978 return
1979 }
1980
1981 var sources []interface{}
1982 sources = append(sources, tx)
1983 util.PushSaveDoc(tree.ID, "tx", sources)
1984
1985 checkUpsertInUserGuide(tree)
1986 }
1987 refreshDynamicRefTexts(tx.nodes, tx.trees)
1988 IncSync()
1989 tx.state.Store(2)
1990 tx.m.Unlock()
1991 return
1992}
1993
1994func (tx *Transaction) rollback() {
1995 tx.trees, tx.nodes = nil, nil
1996 tx.state.Store(3)
1997 tx.m.Unlock()
1998 return
1999}
2000
2001func (tx *Transaction) loadTreeByBlockTree(bt *treenode.BlockTree) (ret *parse.Tree, err error) {
2002 if nil == bt {
2003 return nil, ErrBlockNotFound
2004 }
2005
2006 ret = tx.trees[bt.RootID]
2007 if nil != ret {
2008 return
2009 }
2010
2011 ret, err = filesys.LoadTree(bt.BoxID, bt.Path, tx.luteEngine)
2012 if err != nil {
2013 return
2014 }
2015 tx.trees[bt.RootID] = ret
2016 return
2017}
2018
2019func (tx *Transaction) loadTree(id string) (ret *parse.Tree, err error) {
2020 var rootID, box, p string
2021 bt := treenode.GetBlockTree(id)
2022 if nil == bt {
2023 return nil, ErrBlockNotFound
2024 }
2025 rootID = bt.RootID
2026 box = bt.BoxID
2027 p = bt.Path
2028
2029 ret = tx.trees[rootID]
2030 if nil != ret {
2031 return
2032 }
2033
2034 ret, err = filesys.LoadTree(box, p, tx.luteEngine)
2035 if err != nil {
2036 return
2037 }
2038 tx.trees[rootID] = ret
2039 return
2040}
2041
2042func (tx *Transaction) writeTree(tree *parse.Tree) (err error) {
2043 tx.trees[tree.ID] = tree
2044 treenode.UpsertBlockTree(tree)
2045 return
2046}
2047
2048func getRefsCacheByDefNode(updateNode *ast.Node) (ret []*sql.Ref, changedNodes []*ast.Node) {
2049 changedNodesMap := map[string]*ast.Node{}
2050 ret = sql.GetRefsCacheByDefID(updateNode.ID)
2051 if nil != updateNode.Parent && ast.NodeDocument != updateNode.Parent.Type &&
2052 updateNode.Parent.IsContainerBlock() && updateNode == treenode.FirstLeafBlock(updateNode.Parent) {
2053 // 如果是容器块下第一个叶子块,则需要向上查找引用
2054 for parent := updateNode.Parent; nil != parent; parent = parent.Parent {
2055 if ast.NodeDocument == parent.Type {
2056 break
2057 }
2058
2059 parentRefs := sql.GetRefsCacheByDefID(parent.ID)
2060 if 0 < len(parentRefs) {
2061 ret = append(ret, parentRefs...)
2062 if _, ok := changedNodesMap[parent.ID]; !ok {
2063 changedNodesMap[parent.ID] = parent
2064 }
2065 }
2066 }
2067 }
2068 if ast.NodeDocument != updateNode.Type && updateNode.IsContainerBlock() {
2069 // 如果是容器块,则需要向下查找引用
2070 ast.Walk(updateNode, func(n *ast.Node, entering bool) ast.WalkStatus {
2071 if !entering || !n.IsBlock() {
2072 return ast.WalkContinue
2073 }
2074
2075 childRefs := sql.GetRefsCacheByDefID(n.ID)
2076 if 0 < len(childRefs) {
2077 ret = append(ret, childRefs...)
2078 changedNodesMap[n.ID] = n
2079 }
2080 return ast.WalkContinue
2081 })
2082 }
2083 if ast.NodeHeading == updateNode.Type && "1" == updateNode.IALAttr("fold") {
2084 // 如果是折叠标题,则需要向下查找引用
2085 children := treenode.HeadingChildren(updateNode)
2086 for _, child := range children {
2087 childRefs := sql.GetRefsCacheByDefID(child.ID)
2088 if 0 < len(childRefs) {
2089 ret = append(ret, childRefs...)
2090 changedNodesMap[child.ID] = child
2091 }
2092 }
2093 }
2094 for _, n := range changedNodesMap {
2095 changedNodes = append(changedNodes, n)
2096 }
2097 return
2098}
2099
2100var updateRefTextRenameDocs = map[string]*parse.Tree{}
2101var updateRefTextRenameDocLock = sync.Mutex{}
2102
2103func updateRefTextRenameDoc(renamedTree *parse.Tree) {
2104 updateRefTextRenameDocLock.Lock()
2105 updateRefTextRenameDocs[renamedTree.ID] = renamedTree
2106 updateRefTextRenameDocLock.Unlock()
2107}
2108
2109func FlushUpdateRefTextRenameDocJob() {
2110 sql.FlushQueue()
2111 flushUpdateRefTextRenameDoc()
2112}
2113
2114func flushUpdateRefTextRenameDoc() {
2115 updateRefTextRenameDocLock.Lock()
2116 defer updateRefTextRenameDocLock.Unlock()
2117
2118 for _, tree := range updateRefTextRenameDocs {
2119 refreshDynamicRefText(tree.Root, tree)
2120 }
2121 updateRefTextRenameDocs = map[string]*parse.Tree{}
2122}
2123
2124type changedDefNode struct {
2125 id string
2126 refText string
2127 refType string // ref-d/ref-s/embed
2128}
2129
2130func updateRefText(refNode *ast.Node, changedDefNodes map[string]*ast.Node) (changed bool, defNodes []*changedDefNode) {
2131 ast.Walk(refNode, func(n *ast.Node, entering bool) ast.WalkStatus {
2132 if !entering {
2133 return ast.WalkContinue
2134 }
2135 if treenode.IsBlockRef(n) {
2136 defID, refText, subtype := treenode.GetBlockRef(n)
2137 if "" == defID {
2138 return ast.WalkContinue
2139 }
2140
2141 defNode := changedDefNodes[defID]
2142 if nil == defNode {
2143 return ast.WalkSkipChildren
2144 }
2145
2146 changed = true
2147 if "d" == subtype {
2148 refText = strings.TrimSpace(getNodeRefText(defNode))
2149 if "" == refText {
2150 refText = n.TextMarkBlockRefID
2151 }
2152 treenode.SetDynamicBlockRefText(n, refText)
2153 }
2154 defNodes = append(defNodes, &changedDefNode{id: defID, refText: refText, refType: "ref-" + subtype})
2155 return ast.WalkContinue
2156 } else if treenode.IsEmbedBlockRef(n) {
2157 defID := treenode.GetEmbedBlockRef(n)
2158 changed = true
2159 defNodes = append(defNodes, &changedDefNode{id: defID, refType: "embed"})
2160 return ast.WalkContinue
2161 }
2162 return ast.WalkContinue
2163 })
2164 return
2165}
2166
2167func checkUpsertInUserGuide(tree *parse.Tree) {
2168 // In production mode, data reset warning pops up when editing data in the user guide https://github.com/siyuan-note/siyuan/issues/9757
2169 if "prod" == util.Mode && IsUserGuide(tree.Box) {
2170 util.PushErrMsg(Conf.Language(52), 7000)
2171 }
2172}