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 api
18
19import (
20 "errors"
21 "net/http"
22
23 "github.com/88250/gulu"
24 "github.com/88250/lute"
25 "github.com/88250/lute/ast"
26 "github.com/88250/lute/parse"
27 "github.com/gin-gonic/gin"
28 "github.com/siyuan-note/siyuan/kernel/filesys"
29 "github.com/siyuan-note/siyuan/kernel/model"
30 "github.com/siyuan-note/siyuan/kernel/treenode"
31 "github.com/siyuan-note/siyuan/kernel/util"
32)
33
34func moveOutlineHeading(c *gin.Context) {
35 ret := gulu.Ret.NewResult()
36 defer c.JSON(http.StatusOK, ret)
37
38 arg, ok := util.JsonArg(c, ret)
39 if !ok {
40 return
41 }
42
43 id := arg["id"].(string)
44 if util.InvalidIDPattern(id, ret) {
45 return
46 }
47
48 var parentID, previousID string
49 if nil != arg["parentID"] {
50 parentID = arg["parentID"].(string)
51 if "" != parentID && util.InvalidIDPattern(parentID, ret) {
52 return
53 }
54 }
55 if nil != arg["previousID"] {
56 previousID = arg["previousID"].(string)
57 if "" != previousID && util.InvalidIDPattern(previousID, ret) {
58 return
59 }
60 }
61
62 transactions := []*model.Transaction{
63 {
64 DoOperations: []*model.Operation{
65 {
66 Action: "moveOutlineHeading",
67 ID: id,
68 PreviousID: previousID,
69 ParentID: parentID,
70 },
71 },
72 },
73 }
74
75 model.PerformTransactions(&transactions)
76 model.FlushTxQueue()
77
78 ret.Data = transactions
79 broadcastTransactions(transactions)
80}
81
82func appendDailyNoteBlock(c *gin.Context) {
83 ret := gulu.Ret.NewResult()
84 defer c.JSON(http.StatusOK, ret)
85
86 arg, ok := util.JsonArg(c, ret)
87 if !ok {
88 return
89 }
90
91 data := arg["data"].(string)
92 dataType := arg["dataType"].(string)
93 boxID := arg["notebook"].(string)
94 if util.InvalidIDPattern(boxID, ret) {
95 return
96 }
97 if "markdown" == dataType {
98 luteEngine := util.NewLute()
99 var err error
100 data, err = dataBlockDOM(data, luteEngine)
101 if err != nil {
102 ret.Code = -1
103 ret.Msg = "data block DOM failed: " + err.Error()
104 return
105 }
106 }
107
108 p, _, err := model.CreateDailyNote(boxID)
109 if err != nil {
110 ret.Code = -1
111 ret.Msg = "create daily note failed: " + err.Error()
112 return
113 }
114
115 parentID := util.GetTreeID(p)
116 transactions := []*model.Transaction{
117 {
118 DoOperations: []*model.Operation{
119 {
120 Action: "appendInsert",
121 Data: data,
122 ParentID: parentID,
123 },
124 },
125 },
126 }
127
128 model.PerformTransactions(&transactions)
129 model.FlushTxQueue()
130
131 ret.Data = transactions
132 broadcastTransactions(transactions)
133}
134
135func prependDailyNoteBlock(c *gin.Context) {
136 ret := gulu.Ret.NewResult()
137 defer c.JSON(http.StatusOK, ret)
138
139 arg, ok := util.JsonArg(c, ret)
140 if !ok {
141 return
142 }
143
144 data := arg["data"].(string)
145 dataType := arg["dataType"].(string)
146 boxID := arg["notebook"].(string)
147 if util.InvalidIDPattern(boxID, ret) {
148 return
149 }
150 if "markdown" == dataType {
151 luteEngine := util.NewLute()
152 var err error
153 data, err = dataBlockDOM(data, luteEngine)
154 if err != nil {
155 ret.Code = -1
156 ret.Msg = "data block DOM failed: " + err.Error()
157 return
158 }
159 }
160
161 p, _, err := model.CreateDailyNote(boxID)
162 if err != nil {
163 ret.Code = -1
164 ret.Msg = "create daily note failed: " + err.Error()
165 return
166 }
167
168 parentID := util.GetTreeID(p)
169 transactions := []*model.Transaction{
170 {
171 DoOperations: []*model.Operation{
172 {
173 Action: "prependInsert",
174 Data: data,
175 ParentID: parentID,
176 },
177 },
178 },
179 }
180
181 model.PerformTransactions(&transactions)
182 model.FlushTxQueue()
183
184 ret.Data = transactions
185 broadcastTransactions(transactions)
186}
187
188func unfoldBlock(c *gin.Context) {
189 ret := gulu.Ret.NewResult()
190 defer c.JSON(http.StatusOK, ret)
191
192 arg, ok := util.JsonArg(c, ret)
193 if !ok {
194 return
195 }
196
197 id := arg["id"].(string)
198 if util.InvalidIDPattern(id, ret) {
199 return
200 }
201
202 bt := treenode.GetBlockTree(id)
203 if nil == bt {
204 ret.Code = -1
205 ret.Msg = "block tree not found [id=" + id + "]"
206 return
207 }
208
209 if bt.Type == "d" {
210 ret.Code = -1
211 ret.Msg = "document can not be unfolded"
212 return
213 }
214
215 var transactions []*model.Transaction
216 if "h" == bt.Type {
217 transactions = []*model.Transaction{
218 {
219 DoOperations: []*model.Operation{
220 {
221 Action: "unfoldHeading",
222 ID: id,
223 },
224 },
225 },
226 }
227 } else {
228 data, _ := gulu.JSON.MarshalJSON(map[string]interface{}{"fold": ""})
229 transactions = []*model.Transaction{
230 {
231 DoOperations: []*model.Operation{
232 {
233 Action: "setAttrs",
234 ID: id,
235 Data: string(data),
236 },
237 },
238 },
239 }
240 }
241
242 model.PerformTransactions(&transactions)
243 model.FlushTxQueue()
244
245 broadcastTransactions(transactions)
246}
247
248func foldBlock(c *gin.Context) {
249 ret := gulu.Ret.NewResult()
250 defer c.JSON(http.StatusOK, ret)
251
252 arg, ok := util.JsonArg(c, ret)
253 if !ok {
254 return
255 }
256
257 id := arg["id"].(string)
258 if util.InvalidIDPattern(id, ret) {
259 return
260 }
261
262 bt := treenode.GetBlockTree(id)
263 if nil == bt {
264 ret.Code = -1
265 ret.Msg = "block tree not found [id=" + id + "]"
266 return
267 }
268
269 if bt.Type == "d" {
270 ret.Code = -1
271 ret.Msg = "document can not be folded"
272 return
273 }
274
275 var transactions []*model.Transaction
276 if "h" == bt.Type {
277 transactions = []*model.Transaction{
278 {
279 DoOperations: []*model.Operation{
280 {
281 Action: "foldHeading",
282 ID: id,
283 },
284 },
285 },
286 }
287 } else {
288 data, _ := gulu.JSON.MarshalJSON(map[string]interface{}{"fold": "1"})
289 transactions = []*model.Transaction{
290 {
291 DoOperations: []*model.Operation{
292 {
293 Action: "setAttrs",
294 ID: id,
295 Data: string(data),
296 },
297 },
298 },
299 }
300 }
301
302 model.PerformTransactions(&transactions)
303 model.FlushTxQueue()
304
305 broadcastTransactions(transactions)
306}
307
308func moveBlock(c *gin.Context) {
309 ret := gulu.Ret.NewResult()
310 defer c.JSON(http.StatusOK, ret)
311
312 arg, ok := util.JsonArg(c, ret)
313 if !ok {
314 return
315 }
316
317 id := arg["id"].(string)
318 if util.InvalidIDPattern(id, ret) {
319 return
320 }
321
322 currentBt := treenode.GetBlockTree(id)
323 if nil == currentBt {
324 ret.Code = -1
325 ret.Msg = "block not found [id=" + id + "]"
326 return
327 }
328
329 var parentID, previousID string
330 if nil != arg["parentID"] {
331 parentID = arg["parentID"].(string)
332 if "" != parentID && util.InvalidIDPattern(parentID, ret) {
333 return
334 }
335 }
336 if nil != arg["previousID"] {
337 previousID = arg["previousID"].(string)
338 if "" != previousID && util.InvalidIDPattern(previousID, ret) {
339 return
340 }
341
342 // Check the validity of the API `moveBlock` parameter `previousID` https://github.com/siyuan-note/siyuan/issues/8007
343 if bt := treenode.GetBlockTree(previousID); nil == bt || "d" == bt.Type {
344 ret.Code = -1
345 ret.Msg = "`previousID` can not be the ID of a document"
346 return
347 }
348 }
349
350 var targetBt *treenode.BlockTree
351 if "" != previousID {
352 targetBt = treenode.GetBlockTree(previousID)
353 } else if "" != parentID {
354 targetBt = treenode.GetBlockTree(parentID)
355 }
356
357 if nil == targetBt {
358 ret.Code = -1
359 ret.Msg = "target block not found [id=" + parentID + "]"
360 return
361 }
362
363 transactions := []*model.Transaction{
364 {
365 DoOperations: []*model.Operation{
366 {
367 Action: "move",
368 ID: id,
369 PreviousID: previousID,
370 ParentID: parentID,
371 },
372 },
373 },
374 }
375
376 model.PerformTransactions(&transactions)
377 model.FlushTxQueue()
378
379 model.ReloadProtyle(currentBt.RootID)
380 if currentBt.RootID != targetBt.RootID {
381 model.ReloadProtyle(targetBt.RootID)
382 }
383}
384
385func appendBlock(c *gin.Context) {
386 ret := gulu.Ret.NewResult()
387 defer c.JSON(http.StatusOK, ret)
388
389 arg, ok := util.JsonArg(c, ret)
390 if !ok {
391 return
392 }
393
394 data := arg["data"].(string)
395 dataType := arg["dataType"].(string)
396 parentID := arg["parentID"].(string)
397 if util.InvalidIDPattern(parentID, ret) {
398 return
399 }
400 if "markdown" == dataType {
401 luteEngine := util.NewLute()
402 var err error
403 data, err = dataBlockDOM(data, luteEngine)
404 if err != nil {
405 ret.Code = -1
406 ret.Msg = "data block DOM failed: " + err.Error()
407 return
408 }
409 }
410
411 transactions := []*model.Transaction{
412 {
413 DoOperations: []*model.Operation{
414 {
415 Action: "appendInsert",
416 Data: data,
417 ParentID: parentID,
418 },
419 },
420 },
421 }
422
423 model.PerformTransactions(&transactions)
424 model.FlushTxQueue()
425
426 ret.Data = transactions
427 broadcastTransactions(transactions)
428}
429
430func batchAppendBlock(c *gin.Context) {
431 ret := gulu.Ret.NewResult()
432 defer c.JSON(http.StatusOK, ret)
433
434 arg, ok := util.JsonArg(c, ret)
435 if !ok {
436 return
437 }
438
439 blocksArg := arg["blocks"].([]interface{})
440 var transactions []*model.Transaction
441 luteEngine := util.NewLute()
442 for _, blockArg := range blocksArg {
443 blockMap := blockArg.(map[string]interface{})
444 data := blockMap["data"].(string)
445 dataType := blockMap["dataType"].(string)
446 parentID := blockMap["parentID"].(string)
447 if util.InvalidIDPattern(parentID, ret) {
448 return
449 }
450 if "markdown" == dataType {
451 var err error
452 data, err = dataBlockDOM(data, luteEngine)
453 if err != nil {
454 ret.Code = -1
455 ret.Msg = "data block DOM failed: " + err.Error()
456 return
457 }
458 }
459
460 transactions = append(transactions, &model.Transaction{
461 DoOperations: []*model.Operation{
462 {
463 Action: "appendInsert",
464 Data: data,
465 ParentID: parentID,
466 },
467 },
468 })
469 }
470
471 model.PerformTransactions(&transactions)
472 model.FlushTxQueue()
473
474 ret.Data = transactions
475 broadcastTransactions(transactions)
476}
477
478func prependBlock(c *gin.Context) {
479 ret := gulu.Ret.NewResult()
480 defer c.JSON(http.StatusOK, ret)
481
482 arg, ok := util.JsonArg(c, ret)
483 if !ok {
484 return
485 }
486
487 data := arg["data"].(string)
488 dataType := arg["dataType"].(string)
489 parentID := arg["parentID"].(string)
490 if util.InvalidIDPattern(parentID, ret) {
491 return
492 }
493 if "markdown" == dataType {
494 luteEngine := util.NewLute()
495 var err error
496 data, err = dataBlockDOM(data, luteEngine)
497 if err != nil {
498 ret.Code = -1
499 ret.Msg = "data block DOM failed: " + err.Error()
500 return
501 }
502 }
503
504 transactions := []*model.Transaction{
505 {
506 DoOperations: []*model.Operation{
507 {
508 Action: "prependInsert",
509 Data: data,
510 ParentID: parentID,
511 },
512 },
513 },
514 }
515
516 model.PerformTransactions(&transactions)
517 model.FlushTxQueue()
518
519 ret.Data = transactions
520 broadcastTransactions(transactions)
521}
522
523func batchPrependBlock(c *gin.Context) {
524 ret := gulu.Ret.NewResult()
525 defer c.JSON(http.StatusOK, ret)
526
527 arg, ok := util.JsonArg(c, ret)
528 if !ok {
529 return
530 }
531
532 blocksArg := arg["blocks"].([]interface{})
533 var transactions []*model.Transaction
534 luteEngine := util.NewLute()
535 for _, blockArg := range blocksArg {
536 blockMap := blockArg.(map[string]interface{})
537 data := blockMap["data"].(string)
538 dataType := blockMap["dataType"].(string)
539 parentID := blockMap["parentID"].(string)
540 if util.InvalidIDPattern(parentID, ret) {
541 return
542 }
543 if "markdown" == dataType {
544 var err error
545 data, err = dataBlockDOM(data, luteEngine)
546 if err != nil {
547 ret.Code = -1
548 ret.Msg = "data block DOM failed: " + err.Error()
549 return
550 }
551 }
552
553 transactions = append(transactions, &model.Transaction{
554 DoOperations: []*model.Operation{
555 {
556 Action: "prependInsert",
557 Data: data,
558 ParentID: parentID,
559 },
560 },
561 })
562 }
563
564 model.PerformTransactions(&transactions)
565 model.FlushTxQueue()
566
567 ret.Data = transactions
568 broadcastTransactions(transactions)
569}
570
571func insertBlock(c *gin.Context) {
572 ret := gulu.Ret.NewResult()
573 defer c.JSON(http.StatusOK, ret)
574
575 arg, ok := util.JsonArg(c, ret)
576 if !ok {
577 return
578 }
579
580 data := arg["data"].(string)
581 dataType := arg["dataType"].(string)
582 var parentID, previousID, nextID string
583 if nil != arg["parentID"] {
584 parentID = arg["parentID"].(string)
585 if "" != parentID && util.InvalidIDPattern(parentID, ret) {
586 return
587 }
588 }
589 if nil != arg["previousID"] {
590 previousID = arg["previousID"].(string)
591 if "" != previousID && util.InvalidIDPattern(previousID, ret) {
592 return
593 }
594 }
595 if nil != arg["nextID"] {
596 nextID = arg["nextID"].(string)
597 if "" != nextID && util.InvalidIDPattern(nextID, ret) {
598 return
599 }
600 }
601
602 if "markdown" == dataType {
603 luteEngine := util.NewLute()
604 var err error
605 data, err = dataBlockDOM(data, luteEngine)
606 if err != nil {
607 ret.Code = -1
608 ret.Msg = "data block DOM failed: " + err.Error()
609 return
610 }
611 }
612
613 transactions := []*model.Transaction{
614 {
615 DoOperations: []*model.Operation{
616 {
617 Action: "insert",
618 Data: data,
619 ParentID: parentID,
620 PreviousID: previousID,
621 NextID: nextID,
622 },
623 },
624 },
625 }
626
627 model.PerformTransactions(&transactions)
628 model.FlushTxQueue()
629
630 ret.Data = transactions
631 broadcastTransactions(transactions)
632}
633
634func updateBlock(c *gin.Context) {
635 ret := gulu.Ret.NewResult()
636 defer c.JSON(http.StatusOK, ret)
637
638 arg, ok := util.JsonArg(c, ret)
639 if !ok {
640 return
641 }
642
643 data := arg["data"].(string)
644 dataType := arg["dataType"].(string)
645 id := arg["id"].(string)
646 if util.InvalidIDPattern(id, ret) {
647 return
648 }
649
650 luteEngine := util.NewLute()
651 if "markdown" == dataType {
652 var err error
653 data, err = dataBlockDOM(data, luteEngine)
654 if err != nil {
655 ret.Code = -1
656 ret.Msg = "data block DOM failed: " + err.Error()
657 return
658 }
659 }
660 tree := luteEngine.BlockDOM2Tree(data)
661 if nil == tree || nil == tree.Root || nil == tree.Root.FirstChild {
662 ret.Code = -1
663 ret.Msg = "parse tree failed"
664 return
665 }
666
667 block, err := model.GetBlock(id, nil)
668 if err != nil {
669 ret.Code = -1
670 ret.Msg = "get block failed: " + err.Error()
671 return
672 }
673
674 var transactions []*model.Transaction
675 if "NodeDocument" == block.Type {
676 oldTree, err := filesys.LoadTree(block.Box, block.Path, luteEngine)
677 if err != nil {
678 ret.Code = -1
679 ret.Msg = "load tree failed: " + err.Error()
680 return
681 }
682 var toRemoves []*ast.Node
683 var ops []*model.Operation
684 for n := oldTree.Root.FirstChild; nil != n; n = n.Next {
685 toRemoves = append(toRemoves, n)
686 ops = append(ops, &model.Operation{Action: "delete", ID: n.ID, Data: map[string]interface{}{
687 "createEmptyParagraph": false, // 清空文档后前端不要创建空段落
688 }})
689 }
690 for _, n := range toRemoves {
691 n.Unlink()
692 }
693 ops = append(ops, &model.Operation{Action: "appendInsert", Data: data, ParentID: id})
694 transactions = append(transactions, &model.Transaction{
695 DoOperations: ops,
696 })
697 } else {
698 if "NodeListItem" == block.Type && ast.NodeList == tree.Root.FirstChild.Type {
699 // 使用 API `api/block/updateBlock` 更新列表项时渲染错误 https://github.com/siyuan-note/siyuan/issues/4658
700 tree.Root.AppendChild(tree.Root.FirstChild.FirstChild) // 将列表下的第一个列表项移到文档结尾,移动以后根下面直接挂列表项,渲染器可以正常工作
701 tree.Root.FirstChild.Unlink() // 删除列表
702 tree.Root.FirstChild.Unlink() // 继续删除列表 IAL
703 }
704 tree.Root.FirstChild.SetIALAttr("id", id)
705
706 data = luteEngine.Tree2BlockDOM(tree, luteEngine.RenderOptions)
707 transactions = []*model.Transaction{
708 {
709 DoOperations: []*model.Operation{
710 {
711 Action: "update",
712 ID: id,
713 Data: data,
714 },
715 },
716 },
717 }
718 }
719
720 model.PerformTransactions(&transactions)
721 model.FlushTxQueue()
722
723 ret.Data = transactions
724 broadcastTransactions(transactions)
725}
726
727func batchInsertBlock(c *gin.Context) {
728 ret := gulu.Ret.NewResult()
729 defer c.JSON(http.StatusOK, ret)
730
731 arg, ok := util.JsonArg(c, ret)
732 if !ok {
733 return
734 }
735
736 blocksArg := arg["blocks"].([]interface{})
737 var transactions []*model.Transaction
738 luteEngine := util.NewLute()
739 for _, blockArg := range blocksArg {
740 blockMap := blockArg.(map[string]interface{})
741 data := blockMap["data"].(string)
742 dataType := blockMap["dataType"].(string)
743 var parentID, previousID, nextID string
744 if nil != blockMap["parentID"] {
745 parentID = blockMap["parentID"].(string)
746 if "" != parentID && util.InvalidIDPattern(parentID, ret) {
747 return
748 }
749 }
750 if nil != blockMap["previousID"] {
751 previousID = blockMap["previousID"].(string)
752 if "" != previousID && util.InvalidIDPattern(previousID, ret) {
753 return
754 }
755 }
756 if nil != blockMap["nextID"] {
757 nextID = blockMap["nextID"].(string)
758 if "" != nextID && util.InvalidIDPattern(nextID, ret) {
759 return
760 }
761 }
762
763 if "markdown" == dataType {
764 var err error
765 data, err = dataBlockDOM(data, luteEngine)
766 if err != nil {
767 ret.Code = -1
768 ret.Msg = "data block DOM failed: " + err.Error()
769 return
770 }
771 }
772
773 transactions = append(transactions, &model.Transaction{
774 DoOperations: []*model.Operation{
775 {
776 Action: "insert",
777 Data: data,
778 ParentID: parentID,
779 PreviousID: previousID,
780 NextID: nextID,
781 },
782 },
783 })
784 }
785
786 model.PerformTransactions(&transactions)
787 model.FlushTxQueue()
788
789 ret.Data = transactions
790 broadcastTransactions(transactions)
791}
792
793func batchUpdateBlock(c *gin.Context) {
794 ret := gulu.Ret.NewResult()
795 defer c.JSON(http.StatusOK, ret)
796
797 arg, ok := util.JsonArg(c, ret)
798 if !ok {
799 return
800 }
801
802 blocksArg := arg["blocks"].([]interface{})
803
804 type updateBlockArg struct {
805 ID string
806 Data string
807 DataType string
808 Block *model.Block
809 Tree *parse.Tree
810 }
811
812 var blocks []*updateBlockArg
813 luteEngine := util.NewLute()
814 for _, blockArg := range blocksArg {
815 blockMap := blockArg.(map[string]interface{})
816 id := blockMap["id"].(string)
817 if util.InvalidIDPattern(id, ret) {
818 return
819 }
820
821 data := blockMap["data"].(string)
822 dataType := blockMap["dataType"].(string)
823 if "markdown" == dataType {
824 var err error
825 data, err = dataBlockDOM(data, luteEngine)
826 if err != nil {
827 ret.Code = -1
828 ret.Msg = "data block DOM failed: " + err.Error()
829 return
830 }
831 }
832 tree := luteEngine.BlockDOM2Tree(data)
833 if nil == tree || nil == tree.Root || nil == tree.Root.FirstChild {
834 ret.Code = -1
835 ret.Msg = "parse tree failed"
836 return
837 }
838
839 block, err := model.GetBlock(id, nil)
840 if err != nil {
841 ret.Code = -1
842 ret.Msg = "get block failed: " + err.Error()
843 return
844 }
845
846 blocks = append(blocks, &updateBlockArg{
847 ID: id,
848 Data: data,
849 DataType: dataType,
850 Block: block,
851 Tree: tree,
852 })
853 }
854
855 var ops []*model.Operation
856 tx := &model.Transaction{}
857 transactions := []*model.Transaction{tx}
858 for _, upBlock := range blocks {
859 block := upBlock.Block
860 data := upBlock.Data
861 tree := upBlock.Tree
862 id := upBlock.ID
863 if "NodeDocument" == block.Type {
864 oldTree, err := filesys.LoadTree(block.Box, block.Path, luteEngine)
865 if err != nil {
866 ret.Code = -1
867 ret.Msg = "load tree failed: " + err.Error()
868 return
869 }
870 var toRemoves []*ast.Node
871
872 for n := oldTree.Root.FirstChild; nil != n; n = n.Next {
873 toRemoves = append(toRemoves, n)
874 ops = append(ops, &model.Operation{Action: "delete", ID: n.ID, Data: map[string]interface{}{
875 "createEmptyParagraph": false, // 清空文档后前端不要创建空段落
876 }})
877 }
878 for _, n := range toRemoves {
879 n.Unlink()
880 }
881 ops = append(ops, &model.Operation{Action: "appendInsert", Data: data, ParentID: id})
882 } else {
883 if "NodeListItem" == block.Type && ast.NodeList == tree.Root.FirstChild.Type {
884 // 使用 API `api/block/updateBlock` 更新列表项时渲染错误 https://github.com/siyuan-note/siyuan/issues/4658
885 tree.Root.AppendChild(tree.Root.FirstChild.FirstChild) // 将列表下的第一个列表项移到文档结尾,移动以后根下面直接挂列表项,渲染器可以正常工作
886 tree.Root.FirstChild.Unlink() // 删除列表
887 tree.Root.FirstChild.Unlink() // 继续删除列表 IAL
888 }
889 tree.Root.FirstChild.SetIALAttr("id", id)
890
891 data = luteEngine.Tree2BlockDOM(tree, luteEngine.RenderOptions)
892 ops = append(ops, &model.Operation{
893 Action: "update",
894 ID: id,
895 Data: data,
896 })
897 }
898 }
899
900 tx.DoOperations = ops
901 model.PerformTransactions(&transactions)
902 model.FlushTxQueue()
903
904 ret.Data = transactions
905 broadcastTransactions(transactions)
906
907}
908
909func deleteBlock(c *gin.Context) {
910 ret := gulu.Ret.NewResult()
911 defer c.JSON(http.StatusOK, ret)
912
913 arg, ok := util.JsonArg(c, ret)
914 if !ok {
915 return
916 }
917
918 id := arg["id"].(string)
919 if util.InvalidIDPattern(id, ret) {
920 return
921 }
922
923 transactions := []*model.Transaction{
924 {
925 DoOperations: []*model.Operation{
926 {
927 Action: "delete",
928 ID: id,
929 },
930 },
931 },
932 }
933
934 model.PerformTransactions(&transactions)
935
936 ret.Data = transactions
937 broadcastTransactions(transactions)
938}
939
940func broadcastTransactions(transactions []*model.Transaction) {
941 evt := util.NewCmdResult("transactions", 0, util.PushModeBroadcast)
942 evt.Data = transactions
943 util.PushEvent(evt)
944}
945
946func dataBlockDOM(data string, luteEngine *lute.Lute) (ret string, err error) {
947 luteEngine.SetHTMLTag2TextMark(true) // API `/api/block/**` 无法使用 `<u>foo</u>` 与 `<kbd>bar</kbd>` 插入/更新行内元素 https://github.com/siyuan-note/siyuan/issues/6039
948
949 ret, tree := luteEngine.Md2BlockDOMTree(data, true)
950 if "" == ret {
951 // 使用 API 插入空字符串出现错误 https://github.com/siyuan-note/siyuan/issues/3931
952 blankParagraph := treenode.NewParagraph("")
953 ret = luteEngine.RenderNodeBlockDOM(blankParagraph)
954 }
955
956 invalidID := ""
957 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
958 if !entering {
959 return ast.WalkContinue
960 }
961
962 if "" != n.ID {
963 if !ast.IsNodeIDPattern(n.ID) {
964 invalidID = n.ID
965 return ast.WalkStop
966 }
967 }
968 return ast.WalkContinue
969 })
970
971 if "" != invalidID {
972 err = errors.New("found invalid ID [" + invalidID + "]")
973 ret = ""
974 return
975 }
976 return
977}