template.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  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 model
  17. import (
  18. "bytes"
  19. "errors"
  20. "fmt"
  21. "io/fs"
  22. "os"
  23. "path/filepath"
  24. "sort"
  25. "strings"
  26. "text/template"
  27. "github.com/88250/gulu"
  28. "github.com/88250/lute/ast"
  29. "github.com/88250/lute/parse"
  30. "github.com/88250/lute/render"
  31. "github.com/siyuan-note/filelock"
  32. "github.com/siyuan-note/logging"
  33. "github.com/siyuan-note/siyuan/kernel/av"
  34. "github.com/siyuan-note/siyuan/kernel/search"
  35. "github.com/siyuan-note/siyuan/kernel/sql"
  36. "github.com/siyuan-note/siyuan/kernel/treenode"
  37. "github.com/siyuan-note/siyuan/kernel/util"
  38. )
  39. func RenderGoTemplate(templateContent string) (ret string, err error) {
  40. tmpl := template.New("")
  41. tplFuncMap := util.BuiltInTemplateFuncs()
  42. sql.SQLTemplateFuncs(&tplFuncMap)
  43. tmpl = tmpl.Funcs(tplFuncMap)
  44. tpl, err := tmpl.Parse(templateContent)
  45. if nil != err {
  46. return "", errors.New(fmt.Sprintf(Conf.Language(44), err.Error()))
  47. }
  48. buf := &bytes.Buffer{}
  49. buf.Grow(4096)
  50. err = tpl.Execute(buf, nil)
  51. if nil != err {
  52. return "", errors.New(fmt.Sprintf(Conf.Language(44), err.Error()))
  53. }
  54. ret = buf.String()
  55. return
  56. }
  57. func RemoveTemplate(p string) (err error) {
  58. err = filelock.Remove(p)
  59. if nil != err {
  60. logging.LogErrorf("remove template failed: %s", err)
  61. }
  62. return
  63. }
  64. func SearchTemplate(keyword string) (ret []*Block) {
  65. ret = []*Block{}
  66. templates := filepath.Join(util.DataDir, "templates")
  67. if !util.IsPathRegularDirOrSymlinkDir(templates) {
  68. return
  69. }
  70. groups, err := os.ReadDir(templates)
  71. if nil != err {
  72. logging.LogErrorf("read templates failed: %s", err)
  73. return
  74. }
  75. sort.Slice(ret, func(i, j int) bool {
  76. return util.PinYinCompare(filepath.Base(groups[i].Name()), filepath.Base(groups[j].Name()))
  77. })
  78. k := strings.ToLower(keyword)
  79. for _, group := range groups {
  80. if strings.HasPrefix(group.Name(), ".") {
  81. continue
  82. }
  83. if group.IsDir() {
  84. var templateBlocks []*Block
  85. templateDir := filepath.Join(templates, group.Name())
  86. filelock.Walk(templateDir, func(path string, info fs.FileInfo, err error) error {
  87. name := strings.ToLower(info.Name())
  88. if strings.HasPrefix(name, ".") {
  89. if info.IsDir() {
  90. return filepath.SkipDir
  91. }
  92. return nil
  93. }
  94. if !strings.HasSuffix(name, ".md") || strings.HasPrefix(name, "readme") || !strings.Contains(name, k) {
  95. return nil
  96. }
  97. content := strings.TrimPrefix(path, templates)
  98. content = strings.TrimSuffix(content, ".md")
  99. content = filepath.ToSlash(content)
  100. content = strings.TrimPrefix(content, "/")
  101. _, content = search.MarkText(content, keyword, 32, Conf.Search.CaseSensitive)
  102. b := &Block{Path: path, Content: content}
  103. templateBlocks = append(templateBlocks, b)
  104. return nil
  105. })
  106. sort.Slice(templateBlocks, func(i, j int) bool {
  107. return util.PinYinCompare(filepath.Base(templateBlocks[i].Path), filepath.Base(templateBlocks[j].Path))
  108. })
  109. ret = append(ret, templateBlocks...)
  110. } else {
  111. name := strings.ToLower(group.Name())
  112. if strings.HasPrefix(name, ".") || !strings.HasSuffix(name, ".md") || "readme.md" == name || !strings.Contains(name, k) {
  113. continue
  114. }
  115. content := group.Name()
  116. content = strings.TrimSuffix(content, ".md")
  117. content = filepath.ToSlash(content)
  118. _, content = search.MarkText(content, keyword, 32, Conf.Search.CaseSensitive)
  119. b := &Block{Path: filepath.Join(templates, group.Name()), Content: content}
  120. ret = append(ret, b)
  121. }
  122. }
  123. return
  124. }
  125. func DocSaveAsTemplate(id, name string, overwrite bool) (code int, err error) {
  126. bt := treenode.GetBlockTree(id)
  127. if nil == bt {
  128. return
  129. }
  130. tree := prepareExportTree(bt)
  131. addBlockIALNodes(tree, true)
  132. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  133. if !entering {
  134. return ast.WalkContinue
  135. }
  136. // Code content in templates is not properly escaped https://github.com/siyuan-note/siyuan/issues/9649
  137. switch n.Type {
  138. case ast.NodeCodeBlockCode:
  139. n.Tokens = bytes.ReplaceAll(n.Tokens, []byte("&quot;"), []byte("\""))
  140. case ast.NodeCodeSpanContent:
  141. n.Tokens = bytes.ReplaceAll(n.Tokens, []byte("&quot;"), []byte("\""))
  142. case ast.NodeTextMark:
  143. if n.IsTextMarkType("code") {
  144. n.TextMarkTextContent = strings.ReplaceAll(n.TextMarkTextContent, "&quot;", "\"")
  145. }
  146. }
  147. return ast.WalkContinue
  148. })
  149. luteEngine := NewLute()
  150. formatRenderer := render.NewFormatRenderer(tree, luteEngine.RenderOptions)
  151. md := formatRenderer.Render()
  152. // 单独渲染根节点的 IAL
  153. if 0 < len(tree.Root.KramdownIAL) {
  154. // 把 docIAL 中的 id 调整到第一个
  155. tree.Root.RemoveIALAttr("id")
  156. tree.Root.KramdownIAL = append([][]string{{"id", tree.Root.ID}}, tree.Root.KramdownIAL...)
  157. md = append(md, []byte("\n")...)
  158. md = append(md, parse.IAL2Tokens(tree.Root.KramdownIAL)...)
  159. }
  160. name = util.FilterFileName(name) + ".md"
  161. name = util.TruncateLenFileName(name)
  162. savePath := filepath.Join(util.DataDir, "templates", name)
  163. if filelock.IsExist(savePath) {
  164. if !overwrite {
  165. code = 1
  166. return
  167. }
  168. }
  169. err = filelock.WriteFile(savePath, md)
  170. return
  171. }
  172. func RenderTemplate(p, id string, preview bool) (tree *parse.Tree, dom string, err error) {
  173. tree, err = LoadTreeByBlockID(id)
  174. if nil != err {
  175. return
  176. }
  177. node := treenode.GetNodeInTree(tree, id)
  178. if nil == node {
  179. err = ErrBlockNotFound
  180. return
  181. }
  182. block := sql.BuildBlockFromNode(node, tree)
  183. md, err := os.ReadFile(p)
  184. if nil != err {
  185. return
  186. }
  187. dataModel := map[string]string{}
  188. var titleVar string
  189. if nil != block {
  190. titleVar = block.Name
  191. if "d" == block.Type {
  192. titleVar = block.Content
  193. }
  194. dataModel["title"] = titleVar
  195. dataModel["id"] = block.ID
  196. dataModel["name"] = block.Name
  197. dataModel["alias"] = block.Alias
  198. }
  199. goTpl := template.New("").Delims(".action{", "}")
  200. tplFuncMap := util.BuiltInTemplateFuncs()
  201. sql.SQLTemplateFuncs(&tplFuncMap)
  202. goTpl = goTpl.Funcs(tplFuncMap)
  203. tpl, err := goTpl.Funcs(tplFuncMap).Parse(gulu.Str.FromBytes(md))
  204. if nil != err {
  205. err = errors.New(fmt.Sprintf(Conf.Language(44), err.Error()))
  206. return
  207. }
  208. buf := &bytes.Buffer{}
  209. buf.Grow(4096)
  210. if err = tpl.Execute(buf, dataModel); nil != err {
  211. err = errors.New(fmt.Sprintf(Conf.Language(44), err.Error()))
  212. return
  213. }
  214. md = buf.Bytes()
  215. tree = parseKTree(md)
  216. if nil == tree {
  217. msg := fmt.Sprintf("parse tree [%s] failed", p)
  218. logging.LogErrorf(msg)
  219. err = errors.New(msg)
  220. return
  221. }
  222. var nodesNeedAppendChild, unlinks []*ast.Node
  223. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  224. if !entering {
  225. return ast.WalkContinue
  226. }
  227. if "" != n.ID {
  228. // 重新生成 ID
  229. n.ID = ast.NewNodeID()
  230. n.SetIALAttr("id", n.ID)
  231. n.RemoveIALAttr(av.NodeAttrNameAvs)
  232. // Blocks created via template update time earlier than creation time https://github.com/siyuan-note/siyuan/issues/8607
  233. refreshUpdated(n)
  234. }
  235. if (ast.NodeListItem == n.Type && (nil == n.FirstChild ||
  236. (3 == n.ListData.Typ && (nil == n.FirstChild.Next || ast.NodeKramdownBlockIAL == n.FirstChild.Next.Type)))) ||
  237. (ast.NodeBlockquote == n.Type && nil != n.FirstChild && nil != n.FirstChild.Next && ast.NodeKramdownBlockIAL == n.FirstChild.Next.Type) {
  238. nodesNeedAppendChild = append(nodesNeedAppendChild, n)
  239. }
  240. // 块引缺失锚文本情况下自动补全 https://github.com/siyuan-note/siyuan/issues/6087
  241. if n.IsTextMarkType("block-ref") {
  242. if refText := n.Text(); "" == refText {
  243. refText = sql.GetRefText(n.TextMarkBlockRefID)
  244. if "" != refText {
  245. treenode.SetDynamicBlockRefText(n, refText)
  246. } else {
  247. unlinks = append(unlinks, n)
  248. }
  249. }
  250. } else if n.IsTextMarkType("inline-math") {
  251. if n.ParentIs(ast.NodeTableCell) {
  252. // 表格中的公式中带有管道符时使用 HTML 实体替换管道符 Improve the handling of inline-math containing `|` in the table https://github.com/siyuan-note/siyuan/issues/9227
  253. n.TextMarkInlineMathContent = strings.ReplaceAll(n.TextMarkInlineMathContent, "|", "&#124;")
  254. }
  255. }
  256. if ast.NodeAttributeView == n.Type {
  257. // 重新生成数据库视图
  258. attrView, parseErr := av.ParseAttributeView(n.AttributeViewID)
  259. if nil != parseErr {
  260. logging.LogErrorf("parse attribute view [%s] failed: %s", n.AttributeViewID, parseErr)
  261. } else {
  262. cloned := attrView.ShallowClone()
  263. if nil == cloned {
  264. logging.LogErrorf("clone attribute view [%s] failed", n.AttributeViewID)
  265. return ast.WalkContinue
  266. }
  267. n.AttributeViewID = cloned.ID
  268. if !preview {
  269. // 非预览时持久化数据库
  270. if saveErr := av.SaveAttributeView(cloned); nil != saveErr {
  271. logging.LogErrorf("save attribute view [%s] failed: %s", cloned.ID, saveErr)
  272. }
  273. } else {
  274. // 预览时使用简单表格渲染
  275. viewID := n.IALAttr(av.NodeAttrView)
  276. view, getErr := attrView.GetCurrentView(viewID)
  277. if nil != getErr {
  278. logging.LogErrorf("get attribute view [%s] failed: %s", n.AttributeViewID, getErr)
  279. return ast.WalkContinue
  280. }
  281. table := sql.RenderAttributeViewTable(attrView, view, "", GetBlockAttrsWithoutWaitWriting)
  282. var aligns []int
  283. for range table.Columns {
  284. aligns = append(aligns, 0)
  285. }
  286. mdTable := &ast.Node{Type: ast.NodeTable, TableAligns: aligns}
  287. mdTableHead := &ast.Node{Type: ast.NodeTableHead}
  288. mdTable.AppendChild(mdTableHead)
  289. mdTableHeadRow := &ast.Node{Type: ast.NodeTableRow, TableAligns: aligns}
  290. mdTableHead.AppendChild(mdTableHeadRow)
  291. for _, col := range table.Columns {
  292. cell := &ast.Node{Type: ast.NodeTableCell}
  293. cell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(col.Name)})
  294. mdTableHeadRow.AppendChild(cell)
  295. }
  296. n.InsertBefore(mdTable)
  297. unlinks = append(unlinks, n)
  298. }
  299. }
  300. }
  301. return ast.WalkContinue
  302. })
  303. for _, n := range nodesNeedAppendChild {
  304. if ast.NodeBlockquote == n.Type {
  305. n.FirstChild.InsertAfter(treenode.NewParagraph())
  306. } else {
  307. n.AppendChild(treenode.NewParagraph())
  308. }
  309. }
  310. for _, n := range unlinks {
  311. n.Unlink()
  312. }
  313. // 折叠标题导出为模板后使用会出现内容重复 https://github.com/siyuan-note/siyuan/issues/4488
  314. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  315. if !entering {
  316. return ast.WalkContinue
  317. }
  318. if "1" == n.IALAttr("heading-fold") { // 为标题折叠下方块添加属性,前端渲染以后会统一做移除处理
  319. n.SetIALAttr("status", "temp")
  320. }
  321. return ast.WalkContinue
  322. })
  323. luteEngine := NewLute()
  324. dom = luteEngine.Tree2BlockDOM(tree, luteEngine.RenderOptions)
  325. return
  326. }
  327. func addBlockIALNodes(tree *parse.Tree, removeUpdated bool) {
  328. var blocks []*ast.Node
  329. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  330. if !entering || !n.IsBlock() {
  331. return ast.WalkContinue
  332. }
  333. if ast.NodeBlockQueryEmbed == n.Type {
  334. if script := n.ChildByType(ast.NodeBlockQueryEmbedScript); nil != script {
  335. script.Tokens = bytes.ReplaceAll(script.Tokens, []byte("\n"), []byte(" "))
  336. }
  337. } else if ast.NodeHTMLBlock == n.Type {
  338. n.Tokens = bytes.TrimSpace(n.Tokens)
  339. // 使用 <div> 包裹,否则后续解析时会识别为行级 HTML https://github.com/siyuan-note/siyuan/issues/4244
  340. if !bytes.HasPrefix(n.Tokens, []byte("<div>")) {
  341. n.Tokens = append([]byte("<div>\n"), n.Tokens...)
  342. }
  343. if !bytes.HasSuffix(n.Tokens, []byte("</div>")) {
  344. n.Tokens = append(n.Tokens, []byte("\n</div>")...)
  345. }
  346. }
  347. if removeUpdated {
  348. n.RemoveIALAttr("updated")
  349. }
  350. if 0 < len(n.KramdownIAL) {
  351. blocks = append(blocks, n)
  352. }
  353. return ast.WalkContinue
  354. })
  355. for _, block := range blocks {
  356. block.InsertAfter(&ast.Node{Type: ast.NodeKramdownBlockIAL, Tokens: parse.IAL2Tokens(block.KramdownIAL)})
  357. }
  358. }