render.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  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. "regexp"
  20. "strings"
  21. "github.com/88250/gulu"
  22. "github.com/88250/lute"
  23. "github.com/88250/lute/ast"
  24. "github.com/88250/lute/editor"
  25. "github.com/88250/lute/html"
  26. "github.com/88250/lute/parse"
  27. "github.com/88250/lute/render"
  28. "github.com/siyuan-note/siyuan/kernel/sql"
  29. "github.com/siyuan-note/siyuan/kernel/treenode"
  30. "github.com/siyuan-note/siyuan/kernel/util"
  31. )
  32. func renderOutline(heading *ast.Node, luteEngine *lute.Lute) (ret string) {
  33. if nil == heading {
  34. return ""
  35. }
  36. if ast.NodeDocument == heading.Type {
  37. return heading.IALAttr("title")
  38. }
  39. buf := bytes.Buffer{}
  40. buf.Grow(4096)
  41. ast.Walk(heading, func(n *ast.Node, entering bool) ast.WalkStatus {
  42. if !entering {
  43. switch n.Type {
  44. case ast.NodeHeading:
  45. // Show heading block appearance style in the Outline Panel https://github.com/siyuan-note/siyuan/issues/7872
  46. if style := n.IALAttr("style"); "" != style {
  47. buf.WriteString("</span>")
  48. }
  49. }
  50. return ast.WalkContinue
  51. }
  52. if style := n.IALAttr("style"); "" != style {
  53. if strings.Contains(style, "font-size") { // 大纲字号不应该跟随字体设置 https://github.com/siyuan-note/siyuan/issues/7202
  54. style = regexp.MustCompile("font-size:.*?;").ReplaceAllString(style, "font-size: inherit;")
  55. n.SetIALAttr("style", style)
  56. }
  57. }
  58. switch n.Type {
  59. case ast.NodeHeading:
  60. // Show heading block appearance style in the Outline Panel https://github.com/siyuan-note/siyuan/issues/7872
  61. if style := n.IALAttr("style"); "" != style {
  62. buf.WriteString("<span style=\"")
  63. buf.WriteString(style)
  64. buf.WriteString("\">")
  65. }
  66. case ast.NodeText, ast.NodeLinkText, ast.NodeCodeBlockCode, ast.NodeMathBlockContent:
  67. tokens := html.EscapeHTML(n.Tokens)
  68. tokens = bytes.ReplaceAll(tokens, []byte(" "), []byte("&nbsp;")) // 大纲面板条目中无法显示多个空格 https://github.com/siyuan-note/siyuan/issues/4370
  69. buf.Write(tokens)
  70. case ast.NodeBackslashContent:
  71. buf.Write(html.EscapeHTML(n.Tokens))
  72. case ast.NodeTextMark:
  73. dom := luteEngine.RenderNodeBlockDOM(n)
  74. buf.WriteString(dom)
  75. return ast.WalkSkipChildren
  76. case ast.NodeEmoji:
  77. dom := luteEngine.RenderNodeBlockDOM(n)
  78. buf.WriteString(dom)
  79. return ast.WalkSkipChildren
  80. case ast.NodeImage:
  81. if title := n.ChildByType(ast.NodeLinkTitle); nil != title {
  82. // 标题后直接跟图片时图片的提示文本不再渲染到大纲中 https://github.com/siyuan-note/siyuan/issues/6278
  83. title.Unlink()
  84. }
  85. dom := luteEngine.RenderNodeBlockDOM(n)
  86. buf.WriteString(dom)
  87. return ast.WalkSkipChildren
  88. }
  89. return ast.WalkContinue
  90. })
  91. ret = strings.TrimSpace(buf.String())
  92. ret = strings.ReplaceAll(ret, "\n", "")
  93. return
  94. }
  95. func renderBlockText(node *ast.Node, excludeTypes []string) (ret string) {
  96. if nil == node {
  97. return
  98. }
  99. ret = sql.NodeStaticContent(node, excludeTypes, false, false, false)
  100. ret = strings.TrimSpace(ret)
  101. ret = strings.ReplaceAll(ret, "\n", "")
  102. ret = util.UnescapeHTML(ret)
  103. ret = util.EscapeHTML(ret)
  104. ret = strings.TrimSpace(ret)
  105. if "" == ret {
  106. // 复制内容为空的块作为块引用时粘贴无效 https://github.com/siyuan-note/siyuan/issues/4962
  107. buf := bytes.Buffer{}
  108. ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
  109. if !entering {
  110. return ast.WalkContinue
  111. }
  112. if ast.NodeImage == n.Type {
  113. title := n.ChildByType(ast.NodeLinkTitle)
  114. if nil == title {
  115. alt := n.ChildByType(ast.NodeLinkText)
  116. if nil != alt && 0 < len(alt.Tokens) {
  117. buf.Write(alt.Tokens)
  118. } else {
  119. buf.WriteString("image")
  120. }
  121. } else {
  122. buf.Write(title.Tokens)
  123. }
  124. }
  125. return ast.WalkContinue
  126. })
  127. ret = buf.String()
  128. }
  129. return
  130. }
  131. func renderBlockDOMByNodes(nodes []*ast.Node, luteEngine *lute.Lute) string {
  132. tree := &parse.Tree{Root: &ast.Node{Type: ast.NodeDocument}, Context: &parse.Context{ParseOption: luteEngine.ParseOptions}}
  133. blockRenderer := render.NewProtyleRenderer(tree, luteEngine.RenderOptions)
  134. for _, n := range nodes {
  135. ast.Walk(n, func(node *ast.Node, entering bool) ast.WalkStatus {
  136. rendererFunc := blockRenderer.RendererFuncs[node.Type]
  137. return rendererFunc(node, entering)
  138. })
  139. }
  140. h := strings.TrimSpace(blockRenderer.Writer.String())
  141. if strings.HasPrefix(h, "<li") {
  142. h = "<ul>" + h + "</ul>"
  143. }
  144. return h
  145. }
  146. func renderBlockContentByNodes(nodes []*ast.Node) string {
  147. var subNodes []*ast.Node
  148. for _, n := range nodes {
  149. if ast.NodeDocument == n.Type {
  150. for c := n.FirstChild; nil != c; c = c.Next {
  151. subNodes = append(subNodes, c)
  152. }
  153. } else {
  154. subNodes = append(subNodes, n)
  155. }
  156. }
  157. buf := bytes.Buffer{}
  158. for _, n := range subNodes {
  159. buf.WriteString(sql.NodeStaticContent(n, nil, false, false, false))
  160. }
  161. return buf.String()
  162. }
  163. func resolveEmbedR(n *ast.Node, blockEmbedMode int, luteEngine *lute.Lute, resolved *[]string, depth *int) {
  164. var children []*ast.Node
  165. if ast.NodeHeading == n.Type {
  166. children = append(children, n)
  167. children = append(children, treenode.HeadingChildren(n)...)
  168. } else if ast.NodeDocument == n.Type {
  169. for c := n.FirstChild; nil != c; c = c.Next {
  170. children = append(children, c)
  171. }
  172. } else {
  173. children = append(children, n)
  174. }
  175. *depth++
  176. if 7 < *depth {
  177. return
  178. }
  179. for _, child := range children {
  180. var unlinks []*ast.Node
  181. parentHeadingLevel := 0
  182. for prev := child; nil != prev; prev = prev.Previous {
  183. if ast.NodeHeading == prev.Type {
  184. parentHeadingLevel = prev.HeadingLevel
  185. break
  186. }
  187. }
  188. ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus {
  189. if !entering || !n.IsBlock() {
  190. return ast.WalkContinue
  191. }
  192. if ast.NodeBlockQueryEmbed == n.Type {
  193. if gulu.Str.Contains(n.ID, *resolved) {
  194. return ast.WalkContinue
  195. }
  196. *resolved = append(*resolved, n.ID)
  197. stmt := n.ChildByType(ast.NodeBlockQueryEmbedScript).TokensStr()
  198. stmt = html.UnescapeString(stmt)
  199. stmt = strings.ReplaceAll(stmt, editor.IALValEscNewLine, "\n")
  200. sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, Conf.Search.Limit)
  201. for _, sqlBlock := range sqlBlocks {
  202. if "query_embed" == sqlBlock.Type {
  203. continue
  204. }
  205. subTree, _ := LoadTreeByBlockID(sqlBlock.ID)
  206. if nil == subTree {
  207. continue
  208. }
  209. var md string
  210. if "d" == sqlBlock.Type {
  211. if 0 == blockEmbedMode {
  212. // 嵌入块中出现了大于等于上方非嵌入块的标题时需要降低嵌入块中的标题级别
  213. // Improve export of heading levels in embedded blocks https://github.com/siyuan-note/siyuan/issues/12233 https://github.com/siyuan-note/siyuan/issues/12741
  214. embedTopLevel := 0
  215. ast.Walk(subTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  216. if !entering || ast.NodeHeading != n.Type {
  217. return ast.WalkContinue
  218. }
  219. embedTopLevel = n.HeadingLevel
  220. if parentHeadingLevel >= embedTopLevel {
  221. n.HeadingLevel += parentHeadingLevel - embedTopLevel + 1
  222. if 6 < n.HeadingLevel {
  223. n.HeadingLevel = 6
  224. }
  225. }
  226. return ast.WalkContinue
  227. })
  228. }
  229. md, _ = lute.FormatNodeSync(subTree.Root, luteEngine.ParseOptions, luteEngine.RenderOptions)
  230. } else if "h" == sqlBlock.Type {
  231. h := treenode.GetNodeInTree(subTree, sqlBlock.ID)
  232. var hChildren []*ast.Node
  233. hChildren = append(hChildren, h)
  234. hChildren = append(hChildren, treenode.HeadingChildren(h)...)
  235. if 0 == blockEmbedMode {
  236. embedTopLevel := 0
  237. for _, hChild := range hChildren {
  238. if ast.NodeHeading == hChild.Type {
  239. embedTopLevel = hChild.HeadingLevel
  240. break
  241. }
  242. }
  243. if parentHeadingLevel >= embedTopLevel {
  244. for _, hChild := range hChildren {
  245. if ast.NodeHeading == hChild.Type {
  246. hChild.HeadingLevel += parentHeadingLevel - embedTopLevel + 1
  247. if 6 < hChild.HeadingLevel {
  248. hChild.HeadingLevel = 6
  249. }
  250. }
  251. }
  252. }
  253. }
  254. mdBuf := &bytes.Buffer{}
  255. for _, hChild := range hChildren {
  256. md, _ = lute.FormatNodeSync(hChild, luteEngine.ParseOptions, luteEngine.RenderOptions)
  257. mdBuf.WriteString(md)
  258. mdBuf.WriteString("\n\n")
  259. }
  260. md = mdBuf.String()
  261. } else {
  262. node := treenode.GetNodeInTree(subTree, sqlBlock.ID)
  263. md, _ = lute.FormatNodeSync(node, luteEngine.ParseOptions, luteEngine.RenderOptions)
  264. }
  265. buf := &bytes.Buffer{}
  266. lines := strings.Split(md, "\n")
  267. for i, line := range lines {
  268. if 0 == blockEmbedMode { // 使用原始文本
  269. buf.WriteString(line)
  270. } else { // 使用引述块
  271. buf.WriteString("> " + line)
  272. }
  273. if i < len(lines)-1 {
  274. buf.WriteString("\n")
  275. }
  276. }
  277. buf.WriteString("\n\n")
  278. subTree = parse.Parse("", buf.Bytes(), luteEngine.ParseOptions)
  279. var inserts []*ast.Node
  280. for subNode := subTree.Root.FirstChild; nil != subNode; subNode = subNode.Next {
  281. if ast.NodeKramdownBlockIAL != subNode.Type {
  282. inserts = append(inserts, subNode)
  283. }
  284. }
  285. for _, insert := range inserts {
  286. n.InsertBefore(insert)
  287. if gulu.Str.Contains(sqlBlock.ID, *resolved) {
  288. return ast.WalkContinue
  289. }
  290. resolveEmbedR(insert, blockEmbedMode, luteEngine, resolved, depth)
  291. }
  292. }
  293. unlinks = append(unlinks, n)
  294. return ast.WalkSkipChildren
  295. }
  296. return ast.WalkContinue
  297. })
  298. for _, unlink := range unlinks {
  299. unlink.Unlink()
  300. }
  301. }
  302. return
  303. }
  304. func renderBlockMarkdownR(id string, rendered *[]string) (ret []*ast.Node) {
  305. if gulu.Str.Contains(id, *rendered) {
  306. return
  307. }
  308. *rendered = append(*rendered, id)
  309. b := treenode.GetBlockTree(id)
  310. if nil == b {
  311. return
  312. }
  313. var err error
  314. var t *parse.Tree
  315. if t, err = LoadTreeByBlockID(b.ID); err != nil {
  316. return
  317. }
  318. node := treenode.GetNodeInTree(t, b.ID)
  319. if nil == node {
  320. return
  321. }
  322. var children []*ast.Node
  323. if ast.NodeHeading == node.Type {
  324. children = append(children, node)
  325. children = append(children, treenode.HeadingChildren(node)...)
  326. } else if ast.NodeDocument == node.Type {
  327. for c := node.FirstChild; nil != c; c = c.Next {
  328. children = append(children, c)
  329. }
  330. } else {
  331. children = append(children, node)
  332. }
  333. for _, child := range children {
  334. var unlinks, inserts []*ast.Node
  335. ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus {
  336. if !entering || !n.IsBlock() {
  337. return ast.WalkContinue
  338. }
  339. if ast.NodeBlockQueryEmbed == n.Type {
  340. stmt := n.ChildByType(ast.NodeBlockQueryEmbedScript).TokensStr()
  341. stmt = html.UnescapeString(stmt)
  342. stmt = strings.ReplaceAll(stmt, editor.IALValEscNewLine, "\n")
  343. sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, Conf.Search.Limit)
  344. for _, sqlBlock := range sqlBlocks {
  345. subNodes := renderBlockMarkdownR(sqlBlock.ID, rendered)
  346. for _, subNode := range subNodes {
  347. inserts = append(inserts, subNode)
  348. }
  349. }
  350. unlinks = append(unlinks, n)
  351. return ast.WalkSkipChildren
  352. }
  353. return ast.WalkContinue
  354. })
  355. for _, n := range unlinks {
  356. n.Unlink()
  357. }
  358. if ast.NodeBlockQueryEmbed != child.Type {
  359. ret = append(ret, child)
  360. } else {
  361. for _, n := range inserts {
  362. ret = append(ret, n)
  363. }
  364. }
  365. }
  366. return
  367. }