block_op.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  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. package api
  17. import (
  18. "errors"
  19. "net/http"
  20. "path"
  21. "strings"
  22. "github.com/88250/gulu"
  23. "github.com/88250/lute"
  24. "github.com/88250/lute/ast"
  25. "github.com/gin-gonic/gin"
  26. "github.com/siyuan-note/siyuan/kernel/filesys"
  27. "github.com/siyuan-note/siyuan/kernel/model"
  28. "github.com/siyuan-note/siyuan/kernel/treenode"
  29. "github.com/siyuan-note/siyuan/kernel/util"
  30. )
  31. func moveOutlineHeading(c *gin.Context) {
  32. ret := gulu.Ret.NewResult()
  33. defer c.JSON(http.StatusOK, ret)
  34. arg, ok := util.JsonArg(c, ret)
  35. if !ok {
  36. return
  37. }
  38. id := arg["id"].(string)
  39. if util.InvalidIDPattern(id, ret) {
  40. return
  41. }
  42. var parentID, previousID string
  43. if nil != arg["parentID"] {
  44. parentID = arg["parentID"].(string)
  45. if "" != parentID && util.InvalidIDPattern(parentID, ret) {
  46. return
  47. }
  48. }
  49. if nil != arg["previousID"] {
  50. previousID = arg["previousID"].(string)
  51. if "" != previousID && util.InvalidIDPattern(previousID, ret) {
  52. return
  53. }
  54. }
  55. transactions := []*model.Transaction{
  56. {
  57. DoOperations: []*model.Operation{
  58. {
  59. Action: "moveOutlineHeading",
  60. ID: id,
  61. PreviousID: previousID,
  62. ParentID: parentID,
  63. },
  64. },
  65. },
  66. }
  67. model.PerformTransactions(&transactions)
  68. model.WaitForWritingFiles()
  69. ret.Data = transactions
  70. broadcastTransactions(transactions)
  71. }
  72. func appendDailyNoteBlock(c *gin.Context) {
  73. ret := gulu.Ret.NewResult()
  74. defer c.JSON(http.StatusOK, ret)
  75. arg, ok := util.JsonArg(c, ret)
  76. if !ok {
  77. return
  78. }
  79. data := arg["data"].(string)
  80. dataType := arg["dataType"].(string)
  81. boxID := arg["notebook"].(string)
  82. if util.InvalidIDPattern(boxID, ret) {
  83. return
  84. }
  85. if "markdown" == dataType {
  86. luteEngine := util.NewLute()
  87. var err error
  88. data, err = dataBlockDOM(data, luteEngine)
  89. if nil != err {
  90. ret.Code = -1
  91. ret.Msg = "data block DOM failed: " + err.Error()
  92. return
  93. }
  94. }
  95. p, _, err := model.CreateDailyNote(boxID)
  96. if nil != err {
  97. ret.Code = -1
  98. ret.Msg = "create daily note failed: " + err.Error()
  99. return
  100. }
  101. parentID := strings.TrimSuffix(path.Base(p), ".sy")
  102. transactions := []*model.Transaction{
  103. {
  104. DoOperations: []*model.Operation{
  105. {
  106. Action: "appendInsert",
  107. Data: data,
  108. ParentID: parentID,
  109. },
  110. },
  111. },
  112. }
  113. model.PerformTransactions(&transactions)
  114. model.WaitForWritingFiles()
  115. ret.Data = transactions
  116. broadcastTransactions(transactions)
  117. }
  118. func prependDailyNoteBlock(c *gin.Context) {
  119. ret := gulu.Ret.NewResult()
  120. defer c.JSON(http.StatusOK, ret)
  121. arg, ok := util.JsonArg(c, ret)
  122. if !ok {
  123. return
  124. }
  125. data := arg["data"].(string)
  126. dataType := arg["dataType"].(string)
  127. boxID := arg["notebook"].(string)
  128. if util.InvalidIDPattern(boxID, ret) {
  129. return
  130. }
  131. if "markdown" == dataType {
  132. luteEngine := util.NewLute()
  133. var err error
  134. data, err = dataBlockDOM(data, luteEngine)
  135. if nil != err {
  136. ret.Code = -1
  137. ret.Msg = "data block DOM failed: " + err.Error()
  138. return
  139. }
  140. }
  141. p, _, err := model.CreateDailyNote(boxID)
  142. if nil != err {
  143. ret.Code = -1
  144. ret.Msg = "create daily note failed: " + err.Error()
  145. return
  146. }
  147. parentID := strings.TrimSuffix(path.Base(p), ".sy")
  148. transactions := []*model.Transaction{
  149. {
  150. DoOperations: []*model.Operation{
  151. {
  152. Action: "prependInsert",
  153. Data: data,
  154. ParentID: parentID,
  155. },
  156. },
  157. },
  158. }
  159. model.PerformTransactions(&transactions)
  160. model.WaitForWritingFiles()
  161. ret.Data = transactions
  162. broadcastTransactions(transactions)
  163. }
  164. func unfoldBlock(c *gin.Context) {
  165. ret := gulu.Ret.NewResult()
  166. defer c.JSON(http.StatusOK, ret)
  167. arg, ok := util.JsonArg(c, ret)
  168. if !ok {
  169. return
  170. }
  171. id := arg["id"].(string)
  172. if util.InvalidIDPattern(id, ret) {
  173. return
  174. }
  175. bt := treenode.GetBlockTree(id)
  176. if nil == bt {
  177. ret.Code = -1
  178. ret.Msg = "block tree not found [id=" + id + "]"
  179. return
  180. }
  181. if bt.Type == "d" {
  182. ret.Code = -1
  183. ret.Msg = "document can not be unfolded"
  184. return
  185. }
  186. var transactions []*model.Transaction
  187. if "h" == bt.Type {
  188. transactions = []*model.Transaction{
  189. {
  190. DoOperations: []*model.Operation{
  191. {
  192. Action: "unfoldHeading",
  193. ID: id,
  194. },
  195. },
  196. },
  197. }
  198. } else {
  199. data, _ := gulu.JSON.MarshalJSON(map[string]interface{}{"fold": ""})
  200. transactions = []*model.Transaction{
  201. {
  202. DoOperations: []*model.Operation{
  203. {
  204. Action: "setAttrs",
  205. ID: id,
  206. Data: string(data),
  207. },
  208. },
  209. },
  210. }
  211. }
  212. model.PerformTransactions(&transactions)
  213. model.WaitForWritingFiles()
  214. broadcastTransactions(transactions)
  215. }
  216. func foldBlock(c *gin.Context) {
  217. ret := gulu.Ret.NewResult()
  218. defer c.JSON(http.StatusOK, ret)
  219. arg, ok := util.JsonArg(c, ret)
  220. if !ok {
  221. return
  222. }
  223. id := arg["id"].(string)
  224. if util.InvalidIDPattern(id, ret) {
  225. return
  226. }
  227. bt := treenode.GetBlockTree(id)
  228. if nil == bt {
  229. ret.Code = -1
  230. ret.Msg = "block tree not found [id=" + id + "]"
  231. return
  232. }
  233. if bt.Type == "d" {
  234. ret.Code = -1
  235. ret.Msg = "document can not be folded"
  236. return
  237. }
  238. var transactions []*model.Transaction
  239. if "h" == bt.Type {
  240. transactions = []*model.Transaction{
  241. {
  242. DoOperations: []*model.Operation{
  243. {
  244. Action: "foldHeading",
  245. ID: id,
  246. },
  247. },
  248. },
  249. }
  250. } else {
  251. data, _ := gulu.JSON.MarshalJSON(map[string]interface{}{"fold": "1"})
  252. transactions = []*model.Transaction{
  253. {
  254. DoOperations: []*model.Operation{
  255. {
  256. Action: "setAttrs",
  257. ID: id,
  258. Data: string(data),
  259. },
  260. },
  261. },
  262. }
  263. }
  264. model.PerformTransactions(&transactions)
  265. model.WaitForWritingFiles()
  266. broadcastTransactions(transactions)
  267. }
  268. func moveBlock(c *gin.Context) {
  269. ret := gulu.Ret.NewResult()
  270. defer c.JSON(http.StatusOK, ret)
  271. arg, ok := util.JsonArg(c, ret)
  272. if !ok {
  273. return
  274. }
  275. id := arg["id"].(string)
  276. if util.InvalidIDPattern(id, ret) {
  277. return
  278. }
  279. var parentID, previousID string
  280. if nil != arg["parentID"] {
  281. parentID = arg["parentID"].(string)
  282. if "" != parentID && util.InvalidIDPattern(parentID, ret) {
  283. return
  284. }
  285. }
  286. if nil != arg["previousID"] {
  287. previousID = arg["previousID"].(string)
  288. if "" != previousID && util.InvalidIDPattern(previousID, ret) {
  289. return
  290. }
  291. // Check the validity of the API `moveBlock` parameter `previousID` https://github.com/siyuan-note/siyuan/issues/8007
  292. if bt := treenode.GetBlockTree(previousID); nil == bt || "d" == bt.Type {
  293. ret.Code = -1
  294. ret.Msg = "`previousID` can not be the ID of a document"
  295. return
  296. }
  297. }
  298. transactions := []*model.Transaction{
  299. {
  300. DoOperations: []*model.Operation{
  301. {
  302. Action: "move",
  303. ID: id,
  304. PreviousID: previousID,
  305. ParentID: parentID,
  306. },
  307. },
  308. },
  309. }
  310. model.PerformTransactions(&transactions)
  311. model.WaitForWritingFiles()
  312. ret.Data = transactions
  313. broadcastTransactions(transactions)
  314. }
  315. func appendBlock(c *gin.Context) {
  316. ret := gulu.Ret.NewResult()
  317. defer c.JSON(http.StatusOK, ret)
  318. arg, ok := util.JsonArg(c, ret)
  319. if !ok {
  320. return
  321. }
  322. data := arg["data"].(string)
  323. dataType := arg["dataType"].(string)
  324. parentID := arg["parentID"].(string)
  325. if util.InvalidIDPattern(parentID, ret) {
  326. return
  327. }
  328. if "markdown" == dataType {
  329. luteEngine := util.NewLute()
  330. var err error
  331. data, err = dataBlockDOM(data, luteEngine)
  332. if nil != err {
  333. ret.Code = -1
  334. ret.Msg = "data block DOM failed: " + err.Error()
  335. return
  336. }
  337. }
  338. transactions := []*model.Transaction{
  339. {
  340. DoOperations: []*model.Operation{
  341. {
  342. Action: "appendInsert",
  343. Data: data,
  344. ParentID: parentID,
  345. },
  346. },
  347. },
  348. }
  349. model.PerformTransactions(&transactions)
  350. model.WaitForWritingFiles()
  351. ret.Data = transactions
  352. broadcastTransactions(transactions)
  353. }
  354. func prependBlock(c *gin.Context) {
  355. ret := gulu.Ret.NewResult()
  356. defer c.JSON(http.StatusOK, ret)
  357. arg, ok := util.JsonArg(c, ret)
  358. if !ok {
  359. return
  360. }
  361. data := arg["data"].(string)
  362. dataType := arg["dataType"].(string)
  363. parentID := arg["parentID"].(string)
  364. if util.InvalidIDPattern(parentID, ret) {
  365. return
  366. }
  367. if "markdown" == dataType {
  368. luteEngine := util.NewLute()
  369. var err error
  370. data, err = dataBlockDOM(data, luteEngine)
  371. if nil != err {
  372. ret.Code = -1
  373. ret.Msg = "data block DOM failed: " + err.Error()
  374. return
  375. }
  376. }
  377. transactions := []*model.Transaction{
  378. {
  379. DoOperations: []*model.Operation{
  380. {
  381. Action: "prependInsert",
  382. Data: data,
  383. ParentID: parentID,
  384. },
  385. },
  386. },
  387. }
  388. model.PerformTransactions(&transactions)
  389. model.WaitForWritingFiles()
  390. ret.Data = transactions
  391. broadcastTransactions(transactions)
  392. }
  393. func insertBlock(c *gin.Context) {
  394. ret := gulu.Ret.NewResult()
  395. defer c.JSON(http.StatusOK, ret)
  396. arg, ok := util.JsonArg(c, ret)
  397. if !ok {
  398. return
  399. }
  400. data := arg["data"].(string)
  401. dataType := arg["dataType"].(string)
  402. var parentID, previousID, nextID string
  403. if nil != arg["parentID"] {
  404. parentID = arg["parentID"].(string)
  405. if "" != parentID && util.InvalidIDPattern(parentID, ret) {
  406. return
  407. }
  408. }
  409. if nil != arg["previousID"] {
  410. previousID = arg["previousID"].(string)
  411. if "" != previousID && util.InvalidIDPattern(previousID, ret) {
  412. return
  413. }
  414. }
  415. if nil != arg["nextID"] {
  416. nextID = arg["nextID"].(string)
  417. if "" != nextID && util.InvalidIDPattern(nextID, ret) {
  418. return
  419. }
  420. }
  421. if "markdown" == dataType {
  422. luteEngine := util.NewLute()
  423. var err error
  424. data, err = dataBlockDOM(data, luteEngine)
  425. if nil != err {
  426. ret.Code = -1
  427. ret.Msg = "data block DOM failed: " + err.Error()
  428. return
  429. }
  430. }
  431. transactions := []*model.Transaction{
  432. {
  433. DoOperations: []*model.Operation{
  434. {
  435. Action: "insert",
  436. Data: data,
  437. ParentID: parentID,
  438. PreviousID: previousID,
  439. NextID: nextID,
  440. },
  441. },
  442. },
  443. }
  444. model.PerformTransactions(&transactions)
  445. model.WaitForWritingFiles()
  446. ret.Data = transactions
  447. broadcastTransactions(transactions)
  448. }
  449. func updateBlock(c *gin.Context) {
  450. ret := gulu.Ret.NewResult()
  451. defer c.JSON(http.StatusOK, ret)
  452. arg, ok := util.JsonArg(c, ret)
  453. if !ok {
  454. return
  455. }
  456. data := arg["data"].(string)
  457. dataType := arg["dataType"].(string)
  458. id := arg["id"].(string)
  459. if util.InvalidIDPattern(id, ret) {
  460. return
  461. }
  462. luteEngine := util.NewLute()
  463. if "markdown" == dataType {
  464. var err error
  465. data, err = dataBlockDOM(data, luteEngine)
  466. if nil != err {
  467. ret.Code = -1
  468. ret.Msg = "data block DOM failed: " + err.Error()
  469. return
  470. }
  471. }
  472. tree := luteEngine.BlockDOM2Tree(data)
  473. if nil == tree || nil == tree.Root || nil == tree.Root.FirstChild {
  474. ret.Code = -1
  475. ret.Msg = "parse tree failed"
  476. return
  477. }
  478. block, err := model.GetBlock(id, nil)
  479. if nil != err {
  480. ret.Code = -1
  481. ret.Msg = "get block failed: " + err.Error()
  482. return
  483. }
  484. var transactions []*model.Transaction
  485. if "NodeDocument" == block.Type {
  486. oldTree, err := filesys.LoadTree(block.Box, block.Path, luteEngine)
  487. if nil != err {
  488. ret.Code = -1
  489. ret.Msg = "load tree failed: " + err.Error()
  490. return
  491. }
  492. var toRemoves []*ast.Node
  493. var ops []*model.Operation
  494. for n := oldTree.Root.FirstChild; nil != n; n = n.Next {
  495. toRemoves = append(toRemoves, n)
  496. ops = append(ops, &model.Operation{Action: "delete", ID: n.ID})
  497. }
  498. for _, n := range toRemoves {
  499. n.Unlink()
  500. }
  501. ops = append(ops, &model.Operation{Action: "appendInsert", Data: data, ParentID: id})
  502. transactions = append(transactions, &model.Transaction{
  503. DoOperations: ops,
  504. })
  505. } else {
  506. if "NodeListItem" == block.Type && ast.NodeList == tree.Root.FirstChild.Type {
  507. // 使用 API `api/block/updateBlock` 更新列表项时渲染错误 https://github.com/siyuan-note/siyuan/issues/4658
  508. tree.Root.AppendChild(tree.Root.FirstChild.FirstChild) // 将列表下的第一个列表项移到文档结尾,移动以后根下面直接挂列表项,渲染器可以正常工作
  509. tree.Root.FirstChild.Unlink() // 删除列表
  510. tree.Root.FirstChild.Unlink() // 继续删除列表 IAL
  511. }
  512. tree.Root.FirstChild.SetIALAttr("id", id)
  513. data = luteEngine.Tree2BlockDOM(tree, luteEngine.RenderOptions)
  514. transactions = []*model.Transaction{
  515. {
  516. DoOperations: []*model.Operation{
  517. {
  518. Action: "update",
  519. ID: id,
  520. Data: data,
  521. },
  522. },
  523. },
  524. }
  525. }
  526. model.PerformTransactions(&transactions)
  527. model.WaitForWritingFiles()
  528. ret.Data = transactions
  529. broadcastTransactions(transactions)
  530. }
  531. func deleteBlock(c *gin.Context) {
  532. ret := gulu.Ret.NewResult()
  533. defer c.JSON(http.StatusOK, ret)
  534. arg, ok := util.JsonArg(c, ret)
  535. if !ok {
  536. return
  537. }
  538. id := arg["id"].(string)
  539. if util.InvalidIDPattern(id, ret) {
  540. return
  541. }
  542. transactions := []*model.Transaction{
  543. {
  544. DoOperations: []*model.Operation{
  545. {
  546. Action: "delete",
  547. ID: id,
  548. },
  549. },
  550. },
  551. }
  552. model.PerformTransactions(&transactions)
  553. ret.Data = transactions
  554. broadcastTransactions(transactions)
  555. }
  556. func broadcastTransactions(transactions []*model.Transaction) {
  557. evt := util.NewCmdResult("transactions", 0, util.PushModeBroadcast)
  558. evt.Data = transactions
  559. util.PushEvent(evt)
  560. }
  561. func dataBlockDOM(data string, luteEngine *lute.Lute) (ret string, err error) {
  562. luteEngine.SetHTMLTag2TextMark(true) // API `/api/block/**` 无法使用 `<u>foo</u>` 与 `<kbd>bar</kbd>` 插入/更新行内元素 https://github.com/siyuan-note/siyuan/issues/6039
  563. ret, tree := luteEngine.Md2BlockDOMTree(data, true)
  564. if "" == ret {
  565. // 使用 API 插入空字符串出现错误 https://github.com/siyuan-note/siyuan/issues/3931
  566. blankParagraph := treenode.NewParagraph()
  567. ret = luteEngine.RenderNodeBlockDOM(blankParagraph)
  568. }
  569. invalidID := ""
  570. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  571. if !entering {
  572. return ast.WalkContinue
  573. }
  574. if "" != n.ID {
  575. if !ast.IsNodeIDPattern(n.ID) {
  576. invalidID = n.ID
  577. return ast.WalkStop
  578. }
  579. }
  580. return ast.WalkContinue
  581. })
  582. if "" != invalidID {
  583. err = errors.New("found invalid ID [" + invalidID + "]")
  584. ret = ""
  585. return
  586. }
  587. return
  588. }