A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at upstream/main 977 lines 22 kB view raw
1// SiYuan - Refactor your thinking 2// Copyright (c) 2020-present, b3log.org 3// 4// This program is free software: you can redistribute it and/or modify 5// it under the terms of the GNU Affero General Public License as published by 6// the Free Software Foundation, either version 3 of the License, or 7// (at your option) any later version. 8// 9// This program is distributed in the hope that it will be useful, 10// but WITHOUT ANY WARRANTY; without even the implied warranty of 11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12// GNU Affero General Public License for more details. 13// 14// You should have received a copy of the GNU Affero General Public License 15// along with this program. If not, see <https://www.gnu.org/licenses/>. 16 17package 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}