123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676 |
- // SiYuan - Refactor your thinking
- // Copyright (c) 2020-present, b3log.org
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- package api
- import (
- "errors"
- "net/http"
- "path"
- "strings"
- "github.com/88250/gulu"
- "github.com/88250/lute"
- "github.com/88250/lute/ast"
- "github.com/gin-gonic/gin"
- "github.com/siyuan-note/siyuan/kernel/filesys"
- "github.com/siyuan-note/siyuan/kernel/model"
- "github.com/siyuan-note/siyuan/kernel/treenode"
- "github.com/siyuan-note/siyuan/kernel/util"
- )
- func moveOutlineHeading(c *gin.Context) {
- ret := gulu.Ret.NewResult()
- defer c.JSON(http.StatusOK, ret)
- arg, ok := util.JsonArg(c, ret)
- if !ok {
- return
- }
- id := arg["id"].(string)
- if util.InvalidIDPattern(id, ret) {
- return
- }
- var parentID, previousID string
- if nil != arg["parentID"] {
- parentID = arg["parentID"].(string)
- if "" != parentID && util.InvalidIDPattern(parentID, ret) {
- return
- }
- }
- if nil != arg["previousID"] {
- previousID = arg["previousID"].(string)
- if "" != previousID && util.InvalidIDPattern(previousID, ret) {
- return
- }
- }
- transactions := []*model.Transaction{
- {
- DoOperations: []*model.Operation{
- {
- Action: "moveOutlineHeading",
- ID: id,
- PreviousID: previousID,
- ParentID: parentID,
- },
- },
- },
- }
- model.PerformTransactions(&transactions)
- model.WaitForWritingFiles()
- ret.Data = transactions
- broadcastTransactions(transactions)
- }
- func appendDailyNoteBlock(c *gin.Context) {
- ret := gulu.Ret.NewResult()
- defer c.JSON(http.StatusOK, ret)
- arg, ok := util.JsonArg(c, ret)
- if !ok {
- return
- }
- data := arg["data"].(string)
- dataType := arg["dataType"].(string)
- boxID := arg["notebook"].(string)
- if util.InvalidIDPattern(boxID, ret) {
- return
- }
- if "markdown" == dataType {
- luteEngine := util.NewLute()
- var err error
- data, err = dataBlockDOM(data, luteEngine)
- if nil != err {
- ret.Code = -1
- ret.Msg = "data block DOM failed: " + err.Error()
- return
- }
- }
- p, _, err := model.CreateDailyNote(boxID)
- if nil != err {
- ret.Code = -1
- ret.Msg = "create daily note failed: " + err.Error()
- return
- }
- parentID := strings.TrimSuffix(path.Base(p), ".sy")
- transactions := []*model.Transaction{
- {
- DoOperations: []*model.Operation{
- {
- Action: "appendInsert",
- Data: data,
- ParentID: parentID,
- },
- },
- },
- }
- model.PerformTransactions(&transactions)
- model.WaitForWritingFiles()
- ret.Data = transactions
- broadcastTransactions(transactions)
- }
- func prependDailyNoteBlock(c *gin.Context) {
- ret := gulu.Ret.NewResult()
- defer c.JSON(http.StatusOK, ret)
- arg, ok := util.JsonArg(c, ret)
- if !ok {
- return
- }
- data := arg["data"].(string)
- dataType := arg["dataType"].(string)
- boxID := arg["notebook"].(string)
- if util.InvalidIDPattern(boxID, ret) {
- return
- }
- if "markdown" == dataType {
- luteEngine := util.NewLute()
- var err error
- data, err = dataBlockDOM(data, luteEngine)
- if nil != err {
- ret.Code = -1
- ret.Msg = "data block DOM failed: " + err.Error()
- return
- }
- }
- p, _, err := model.CreateDailyNote(boxID)
- if nil != err {
- ret.Code = -1
- ret.Msg = "create daily note failed: " + err.Error()
- return
- }
- parentID := strings.TrimSuffix(path.Base(p), ".sy")
- transactions := []*model.Transaction{
- {
- DoOperations: []*model.Operation{
- {
- Action: "prependInsert",
- Data: data,
- ParentID: parentID,
- },
- },
- },
- }
- model.PerformTransactions(&transactions)
- model.WaitForWritingFiles()
- ret.Data = transactions
- broadcastTransactions(transactions)
- }
- func unfoldBlock(c *gin.Context) {
- ret := gulu.Ret.NewResult()
- defer c.JSON(http.StatusOK, ret)
- arg, ok := util.JsonArg(c, ret)
- if !ok {
- return
- }
- id := arg["id"].(string)
- if util.InvalidIDPattern(id, ret) {
- return
- }
- bt := treenode.GetBlockTree(id)
- if nil == bt {
- ret.Code = -1
- ret.Msg = "block tree not found [id=" + id + "]"
- return
- }
- if bt.Type == "d" {
- ret.Code = -1
- ret.Msg = "document can not be unfolded"
- return
- }
- var transactions []*model.Transaction
- if "h" == bt.Type {
- transactions = []*model.Transaction{
- {
- DoOperations: []*model.Operation{
- {
- Action: "unfoldHeading",
- ID: id,
- },
- },
- },
- }
- } else {
- data, _ := gulu.JSON.MarshalJSON(map[string]interface{}{"fold": ""})
- transactions = []*model.Transaction{
- {
- DoOperations: []*model.Operation{
- {
- Action: "setAttrs",
- ID: id,
- Data: string(data),
- },
- },
- },
- }
- }
- model.PerformTransactions(&transactions)
- model.WaitForWritingFiles()
- broadcastTransactions(transactions)
- }
- func foldBlock(c *gin.Context) {
- ret := gulu.Ret.NewResult()
- defer c.JSON(http.StatusOK, ret)
- arg, ok := util.JsonArg(c, ret)
- if !ok {
- return
- }
- id := arg["id"].(string)
- if util.InvalidIDPattern(id, ret) {
- return
- }
- bt := treenode.GetBlockTree(id)
- if nil == bt {
- ret.Code = -1
- ret.Msg = "block tree not found [id=" + id + "]"
- return
- }
- if bt.Type == "d" {
- ret.Code = -1
- ret.Msg = "document can not be folded"
- return
- }
- var transactions []*model.Transaction
- if "h" == bt.Type {
- transactions = []*model.Transaction{
- {
- DoOperations: []*model.Operation{
- {
- Action: "foldHeading",
- ID: id,
- },
- },
- },
- }
- } else {
- data, _ := gulu.JSON.MarshalJSON(map[string]interface{}{"fold": "1"})
- transactions = []*model.Transaction{
- {
- DoOperations: []*model.Operation{
- {
- Action: "setAttrs",
- ID: id,
- Data: string(data),
- },
- },
- },
- }
- }
- model.PerformTransactions(&transactions)
- model.WaitForWritingFiles()
- broadcastTransactions(transactions)
- }
- func moveBlock(c *gin.Context) {
- ret := gulu.Ret.NewResult()
- defer c.JSON(http.StatusOK, ret)
- arg, ok := util.JsonArg(c, ret)
- if !ok {
- return
- }
- id := arg["id"].(string)
- if util.InvalidIDPattern(id, ret) {
- return
- }
- var parentID, previousID string
- if nil != arg["parentID"] {
- parentID = arg["parentID"].(string)
- if "" != parentID && util.InvalidIDPattern(parentID, ret) {
- return
- }
- }
- if nil != arg["previousID"] {
- previousID = arg["previousID"].(string)
- if "" != previousID && util.InvalidIDPattern(previousID, ret) {
- return
- }
- // Check the validity of the API `moveBlock` parameter `previousID` https://github.com/siyuan-note/siyuan/issues/8007
- if bt := treenode.GetBlockTree(previousID); nil == bt || "d" == bt.Type {
- ret.Code = -1
- ret.Msg = "`previousID` can not be the ID of a document"
- return
- }
- }
- transactions := []*model.Transaction{
- {
- DoOperations: []*model.Operation{
- {
- Action: "move",
- ID: id,
- PreviousID: previousID,
- ParentID: parentID,
- },
- },
- },
- }
- model.PerformTransactions(&transactions)
- model.WaitForWritingFiles()
- ret.Data = transactions
- broadcastTransactions(transactions)
- }
- func appendBlock(c *gin.Context) {
- ret := gulu.Ret.NewResult()
- defer c.JSON(http.StatusOK, ret)
- arg, ok := util.JsonArg(c, ret)
- if !ok {
- return
- }
- data := arg["data"].(string)
- dataType := arg["dataType"].(string)
- parentID := arg["parentID"].(string)
- if util.InvalidIDPattern(parentID, ret) {
- return
- }
- if "markdown" == dataType {
- luteEngine := util.NewLute()
- var err error
- data, err = dataBlockDOM(data, luteEngine)
- if nil != err {
- ret.Code = -1
- ret.Msg = "data block DOM failed: " + err.Error()
- return
- }
- }
- transactions := []*model.Transaction{
- {
- DoOperations: []*model.Operation{
- {
- Action: "appendInsert",
- Data: data,
- ParentID: parentID,
- },
- },
- },
- }
- model.PerformTransactions(&transactions)
- model.WaitForWritingFiles()
- ret.Data = transactions
- broadcastTransactions(transactions)
- }
- func prependBlock(c *gin.Context) {
- ret := gulu.Ret.NewResult()
- defer c.JSON(http.StatusOK, ret)
- arg, ok := util.JsonArg(c, ret)
- if !ok {
- return
- }
- data := arg["data"].(string)
- dataType := arg["dataType"].(string)
- parentID := arg["parentID"].(string)
- if util.InvalidIDPattern(parentID, ret) {
- return
- }
- if "markdown" == dataType {
- luteEngine := util.NewLute()
- var err error
- data, err = dataBlockDOM(data, luteEngine)
- if nil != err {
- ret.Code = -1
- ret.Msg = "data block DOM failed: " + err.Error()
- return
- }
- }
- transactions := []*model.Transaction{
- {
- DoOperations: []*model.Operation{
- {
- Action: "prependInsert",
- Data: data,
- ParentID: parentID,
- },
- },
- },
- }
- model.PerformTransactions(&transactions)
- model.WaitForWritingFiles()
- ret.Data = transactions
- broadcastTransactions(transactions)
- }
- func insertBlock(c *gin.Context) {
- ret := gulu.Ret.NewResult()
- defer c.JSON(http.StatusOK, ret)
- arg, ok := util.JsonArg(c, ret)
- if !ok {
- return
- }
- data := arg["data"].(string)
- dataType := arg["dataType"].(string)
- var parentID, previousID, nextID string
- if nil != arg["parentID"] {
- parentID = arg["parentID"].(string)
- if "" != parentID && util.InvalidIDPattern(parentID, ret) {
- return
- }
- }
- if nil != arg["previousID"] {
- previousID = arg["previousID"].(string)
- if "" != previousID && util.InvalidIDPattern(previousID, ret) {
- return
- }
- }
- if nil != arg["nextID"] {
- nextID = arg["nextID"].(string)
- if "" != nextID && util.InvalidIDPattern(nextID, ret) {
- return
- }
- }
- if "markdown" == dataType {
- luteEngine := util.NewLute()
- var err error
- data, err = dataBlockDOM(data, luteEngine)
- if nil != err {
- ret.Code = -1
- ret.Msg = "data block DOM failed: " + err.Error()
- return
- }
- }
- transactions := []*model.Transaction{
- {
- DoOperations: []*model.Operation{
- {
- Action: "insert",
- Data: data,
- ParentID: parentID,
- PreviousID: previousID,
- NextID: nextID,
- },
- },
- },
- }
- model.PerformTransactions(&transactions)
- model.WaitForWritingFiles()
- ret.Data = transactions
- broadcastTransactions(transactions)
- }
- func updateBlock(c *gin.Context) {
- ret := gulu.Ret.NewResult()
- defer c.JSON(http.StatusOK, ret)
- arg, ok := util.JsonArg(c, ret)
- if !ok {
- return
- }
- data := arg["data"].(string)
- dataType := arg["dataType"].(string)
- id := arg["id"].(string)
- if util.InvalidIDPattern(id, ret) {
- return
- }
- luteEngine := util.NewLute()
- if "markdown" == dataType {
- var err error
- data, err = dataBlockDOM(data, luteEngine)
- if nil != err {
- ret.Code = -1
- ret.Msg = "data block DOM failed: " + err.Error()
- return
- }
- }
- tree := luteEngine.BlockDOM2Tree(data)
- if nil == tree || nil == tree.Root || nil == tree.Root.FirstChild {
- ret.Code = -1
- ret.Msg = "parse tree failed"
- return
- }
- block, err := model.GetBlock(id, nil)
- if nil != err {
- ret.Code = -1
- ret.Msg = "get block failed: " + err.Error()
- return
- }
- var transactions []*model.Transaction
- if "NodeDocument" == block.Type {
- oldTree, err := filesys.LoadTree(block.Box, block.Path, luteEngine)
- if nil != err {
- ret.Code = -1
- ret.Msg = "load tree failed: " + err.Error()
- return
- }
- var toRemoves []*ast.Node
- var ops []*model.Operation
- for n := oldTree.Root.FirstChild; nil != n; n = n.Next {
- toRemoves = append(toRemoves, n)
- ops = append(ops, &model.Operation{Action: "delete", ID: n.ID})
- }
- for _, n := range toRemoves {
- n.Unlink()
- }
- ops = append(ops, &model.Operation{Action: "appendInsert", Data: data, ParentID: id})
- transactions = append(transactions, &model.Transaction{
- DoOperations: ops,
- })
- } else {
- if "NodeListItem" == block.Type && ast.NodeList == tree.Root.FirstChild.Type {
- // 使用 API `api/block/updateBlock` 更新列表项时渲染错误 https://github.com/siyuan-note/siyuan/issues/4658
- tree.Root.AppendChild(tree.Root.FirstChild.FirstChild) // 将列表下的第一个列表项移到文档结尾,移动以后根下面直接挂列表项,渲染器可以正常工作
- tree.Root.FirstChild.Unlink() // 删除列表
- tree.Root.FirstChild.Unlink() // 继续删除列表 IAL
- }
- tree.Root.FirstChild.SetIALAttr("id", id)
- data = luteEngine.Tree2BlockDOM(tree, luteEngine.RenderOptions)
- transactions = []*model.Transaction{
- {
- DoOperations: []*model.Operation{
- {
- Action: "update",
- ID: id,
- Data: data,
- },
- },
- },
- }
- }
- model.PerformTransactions(&transactions)
- model.WaitForWritingFiles()
- ret.Data = transactions
- broadcastTransactions(transactions)
- }
- func deleteBlock(c *gin.Context) {
- ret := gulu.Ret.NewResult()
- defer c.JSON(http.StatusOK, ret)
- arg, ok := util.JsonArg(c, ret)
- if !ok {
- return
- }
- id := arg["id"].(string)
- if util.InvalidIDPattern(id, ret) {
- return
- }
- transactions := []*model.Transaction{
- {
- DoOperations: []*model.Operation{
- {
- Action: "delete",
- ID: id,
- },
- },
- },
- }
- model.PerformTransactions(&transactions)
- ret.Data = transactions
- broadcastTransactions(transactions)
- }
- func broadcastTransactions(transactions []*model.Transaction) {
- evt := util.NewCmdResult("transactions", 0, util.PushModeBroadcast)
- evt.Data = transactions
- util.PushEvent(evt)
- }
- func dataBlockDOM(data string, luteEngine *lute.Lute) (ret string, err error) {
- luteEngine.SetHTMLTag2TextMark(true) // API `/api/block/**` 无法使用 `<u>foo</u>` 与 `<kbd>bar</kbd>` 插入/更新行内元素 https://github.com/siyuan-note/siyuan/issues/6039
- ret, tree := luteEngine.Md2BlockDOMTree(data, true)
- if "" == ret {
- // 使用 API 插入空字符串出现错误 https://github.com/siyuan-note/siyuan/issues/3931
- blankParagraph := treenode.NewParagraph()
- ret = luteEngine.RenderNodeBlockDOM(blankParagraph)
- }
- invalidID := ""
- ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
- if !entering {
- return ast.WalkContinue
- }
- if "" != n.ID {
- if !ast.IsNodeIDPattern(n.ID) {
- invalidID = n.ID
- return ast.WalkStop
- }
- }
- return ast.WalkContinue
- })
- if "" != invalidID {
- err = errors.New("found invalid ID [" + invalidID + "]")
- ret = ""
- return
- }
- return
- }
|