A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
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 model
18
19import (
20 "bytes"
21 "regexp"
22 "strconv"
23 "strings"
24
25 "github.com/88250/gulu"
26 "github.com/88250/lute"
27 "github.com/88250/lute/ast"
28 "github.com/88250/lute/editor"
29 "github.com/88250/lute/html"
30 "github.com/88250/lute/parse"
31 "github.com/88250/lute/render"
32 "github.com/siyuan-note/siyuan/kernel/av"
33 "github.com/siyuan-note/siyuan/kernel/sql"
34 "github.com/siyuan-note/siyuan/kernel/treenode"
35 "github.com/siyuan-note/siyuan/kernel/util"
36)
37
38func renderOutline(heading *ast.Node, luteEngine *lute.Lute) (ret string) {
39 if nil == heading {
40 return ""
41 }
42
43 if ast.NodeDocument == heading.Type {
44 return heading.IALAttr("title")
45 }
46
47 buf := bytes.Buffer{}
48 buf.Grow(4096)
49 ast.Walk(heading, func(n *ast.Node, entering bool) ast.WalkStatus {
50 if !entering {
51 switch n.Type {
52 case ast.NodeHeading:
53 // Show heading block appearance style in the Outline Panel https://github.com/siyuan-note/siyuan/issues/7872
54 if style := n.IALAttr("style"); "" != style {
55 buf.WriteString("</span>")
56 }
57 }
58 return ast.WalkContinue
59 }
60
61 if style := n.IALAttr("style"); "" != style {
62 if strings.Contains(style, "font-size") { // 大纲字号不应该跟随字体设置 https://github.com/siyuan-note/siyuan/issues/7202
63 style = regexp.MustCompile("font-size:.*?;").ReplaceAllString(style, "font-size: inherit;")
64 n.SetIALAttr("style", style)
65 }
66 }
67
68 switch n.Type {
69 case ast.NodeHeading:
70 // Show heading block appearance style in the Outline Panel https://github.com/siyuan-note/siyuan/issues/7872
71 if style := n.IALAttr("style"); "" != style {
72 buf.WriteString("<span style=\"")
73 buf.WriteString(style)
74 buf.WriteString("\">")
75 }
76 case ast.NodeText, ast.NodeLinkText, ast.NodeCodeBlockCode, ast.NodeMathBlockContent:
77 tokens := html.EscapeHTML(n.Tokens)
78 tokens = bytes.ReplaceAll(tokens, []byte(" "), []byte(" ")) // 大纲面板条目中无法显示多个空格 https://github.com/siyuan-note/siyuan/issues/4370
79 buf.Write(tokens)
80 case ast.NodeBackslashContent:
81 buf.Write(html.EscapeHTML(n.Tokens))
82 case ast.NodeTextMark:
83 dom := luteEngine.RenderNodeBlockDOM(n)
84 buf.WriteString(dom)
85 return ast.WalkSkipChildren
86 case ast.NodeEmoji:
87 dom := luteEngine.RenderNodeBlockDOM(n)
88 buf.WriteString(dom)
89 return ast.WalkSkipChildren
90 case ast.NodeImage:
91 if title := n.ChildByType(ast.NodeLinkTitle); nil != title {
92 // 标题后直接跟图片时图片的提示文本不再渲染到大纲中 https://github.com/siyuan-note/siyuan/issues/6278
93 title.Unlink()
94 }
95 dom := luteEngine.RenderNodeBlockDOM(n)
96 buf.WriteString(dom)
97 return ast.WalkSkipChildren
98 }
99 return ast.WalkContinue
100 })
101
102 ret = strings.TrimSpace(buf.String())
103 ret = strings.ReplaceAll(ret, "\n", "")
104 return
105}
106
107func renderBlockText(node *ast.Node, excludeTypes []string, removeLineBreak bool) (ret string) {
108 if nil == node {
109 return
110 }
111
112 ret = sql.NodeStaticContent(node, excludeTypes, false, false, false)
113 ret = strings.TrimSpace(ret)
114 if removeLineBreak {
115 ret = strings.ReplaceAll(ret, "\n", "")
116 }
117 ret = util.UnescapeHTML(ret)
118 ret = util.EscapeHTML(ret)
119 ret = strings.TrimSpace(ret)
120 if "" == ret {
121 // 复制内容为空的块作为块引用时粘贴无效 https://github.com/siyuan-note/siyuan/issues/4962
122 buf := bytes.Buffer{}
123 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
124 if !entering {
125 return ast.WalkContinue
126 }
127
128 if ast.NodeImage == n.Type {
129 title := n.ChildByType(ast.NodeLinkTitle)
130 if nil == title {
131 alt := n.ChildByType(ast.NodeLinkText)
132 if nil != alt && 0 < len(alt.Tokens) {
133 buf.Write(alt.Tokens)
134 } else {
135 buf.WriteString("image")
136 }
137 } else {
138 buf.Write(title.Tokens)
139 }
140 }
141 return ast.WalkContinue
142 })
143 ret = buf.String()
144 }
145 return
146}
147
148func fillBlockRefCount(nodes []*ast.Node) {
149 var defIDs []string
150 for _, n := range nodes {
151 ast.Walk(n, func(n *ast.Node, entering bool) ast.WalkStatus {
152 if !entering {
153 return ast.WalkContinue
154 }
155
156 if n.IsBlock() {
157 defIDs = append(defIDs, n.ID)
158 }
159 return ast.WalkContinue
160 })
161 }
162 defIDs = gulu.Str.RemoveDuplicatedElem(defIDs)
163 refCount := sql.QueryRefCount(defIDs)
164 for _, n := range nodes {
165 ast.Walk(n, func(n *ast.Node, entering bool) ast.WalkStatus {
166 if !entering || !n.IsBlock() {
167 return ast.WalkContinue
168 }
169
170 if cnt := refCount[n.ID]; 0 < cnt {
171 n.SetIALAttr("refcount", strconv.Itoa(cnt))
172 }
173 return ast.WalkContinue
174 })
175 }
176}
177
178func renderBlockDOMByNodes(nodes []*ast.Node, luteEngine *lute.Lute) string {
179 tree := &parse.Tree{Root: &ast.Node{Type: ast.NodeDocument}, Context: &parse.Context{ParseOption: luteEngine.ParseOptions}}
180 blockRenderer := render.NewProtyleRenderer(tree, luteEngine.RenderOptions)
181 for _, node := range nodes {
182 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
183 if entering {
184 if n.IsBlock() {
185 if avs := n.IALAttr(av.NodeAttrNameAvs); "" != avs {
186 // 填充属性视图角标 Display the database title on the block superscript https://github.com/siyuan-note/siyuan/issues/10545
187 avNames := getAvNames(n.IALAttr(av.NodeAttrNameAvs))
188 if "" != avNames {
189 n.SetIALAttr(av.NodeAttrViewNames, avNames)
190 }
191 }
192 }
193 }
194
195 rendererFunc := blockRenderer.RendererFuncs[n.Type]
196 return rendererFunc(n, entering)
197 })
198 }
199 h := strings.TrimSpace(blockRenderer.Writer.String())
200 if strings.HasPrefix(h, "<li") {
201 h = "<ul>" + h + "</ul>"
202 }
203 return h
204}
205
206func renderBlockContentByNodes(nodes []*ast.Node) string {
207 var subNodes []*ast.Node
208 for _, n := range nodes {
209 if ast.NodeDocument == n.Type {
210 for c := n.FirstChild; nil != c; c = c.Next {
211 subNodes = append(subNodes, c)
212 }
213 } else {
214 subNodes = append(subNodes, n)
215 }
216 }
217
218 buf := bytes.Buffer{}
219 for _, n := range subNodes {
220 buf.WriteString(sql.NodeStaticContent(n, nil, false, false, false))
221 }
222 return buf.String()
223}
224
225func resolveEmbedR(n *ast.Node, blockEmbedMode int, luteEngine *lute.Lute, resolved *[]string, depth *int) {
226 var children []*ast.Node
227 if ast.NodeHeading == n.Type {
228 children = append(children, n)
229 children = append(children, treenode.HeadingChildren(n)...)
230 } else if ast.NodeDocument == n.Type {
231 for c := n.FirstChild; nil != c; c = c.Next {
232 children = append(children, c)
233 }
234 } else {
235 children = append(children, n)
236 }
237
238 *depth++
239 if 7 < *depth {
240 return
241 }
242
243 for _, child := range children {
244 var unlinks []*ast.Node
245
246 parentHeadingLevel := 0
247 for prev := child; nil != prev; prev = prev.Previous {
248 if ast.NodeHeading == prev.Type {
249 parentHeadingLevel = prev.HeadingLevel
250 break
251 }
252 }
253
254 ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus {
255 if !entering || !n.IsBlock() {
256 return ast.WalkContinue
257 }
258
259 if ast.NodeBlockQueryEmbed == n.Type {
260 if gulu.Str.Contains(n.ID, *resolved) {
261 return ast.WalkContinue
262 }
263 *resolved = append(*resolved, n.ID)
264
265 stmt := n.ChildByType(ast.NodeBlockQueryEmbedScript).TokensStr()
266 stmt = html.UnescapeString(stmt)
267 stmt = strings.ReplaceAll(stmt, editor.IALValEscNewLine, "\n")
268 sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, Conf.Search.Limit)
269 for _, sqlBlock := range sqlBlocks {
270 if "query_embed" == sqlBlock.Type {
271 continue
272 }
273
274 subTree, _ := LoadTreeByBlockID(sqlBlock.ID)
275 if nil == subTree {
276 continue
277 }
278
279 var md string
280 if "d" == sqlBlock.Type {
281 if 0 == blockEmbedMode {
282 // 嵌入块中出现了大于等于上方非嵌入块的标题时需要降低嵌入块中的标题级别
283 // Improve export of heading levels in embedded blocks https://github.com/siyuan-note/siyuan/issues/12233 https://github.com/siyuan-note/siyuan/issues/12741
284 embedTopLevel := 0
285 ast.Walk(subTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
286 if !entering || ast.NodeHeading != n.Type {
287 return ast.WalkContinue
288 }
289
290 embedTopLevel = n.HeadingLevel
291 if parentHeadingLevel >= embedTopLevel {
292 n.HeadingLevel += parentHeadingLevel - embedTopLevel + 1
293 if 6 < n.HeadingLevel {
294 n.HeadingLevel = 6
295 }
296 }
297 return ast.WalkContinue
298 })
299 }
300
301 md, _ = lute.FormatNodeSync(subTree.Root, luteEngine.ParseOptions, luteEngine.RenderOptions)
302 } else if "h" == sqlBlock.Type {
303 h := treenode.GetNodeInTree(subTree, sqlBlock.ID)
304 var hChildren []*ast.Node
305
306 // 从嵌入块的 IAL 属性中解析 custom-heading-mode,使用全局配置作为默认值
307 blockHeadingMode := Conf.Editor.HeadingEmbedMode
308 if customHeadingMode := n.IALAttr("custom-heading-mode"); "" != customHeadingMode {
309 if mode, err := strconv.Atoi(customHeadingMode); nil == err && (mode == 0 || mode == 1 || mode == 2) {
310 blockHeadingMode = mode
311 }
312 }
313
314 // 根据 blockHeadingMode 处理标题块的显示
315 // blockHeadingMode: 0=显示标题与下方的块,1=仅显示标题,2=仅显示标题下方的块
316 if 1 == blockHeadingMode {
317 // 仅显示标题
318 hChildren = append(hChildren, h)
319 } else if 2 == blockHeadingMode {
320 // 仅显示标题下方的块(默认行为)
321 if "1" != h.IALAttr("fold") {
322 children := treenode.HeadingChildren(h)
323 for _, c := range children {
324 if "1" == c.IALAttr("heading-fold") {
325 // 嵌入块包含折叠标题时不应该显示其下方块 https://github.com/siyuan-note/siyuan/issues/4765
326 continue
327 }
328 hChildren = append(hChildren, c)
329 }
330 }
331 } else {
332 // 0: 显示标题与下方的块
333 hChildren = append(hChildren, h)
334 hChildren = append(hChildren, treenode.HeadingChildren(h)...)
335 }
336 if 0 == blockEmbedMode {
337 embedTopLevel := 0
338 for _, hChild := range hChildren {
339 if ast.NodeHeading == hChild.Type {
340 embedTopLevel = hChild.HeadingLevel
341 break
342 }
343 }
344 if parentHeadingLevel >= embedTopLevel {
345 for _, hChild := range hChildren {
346 if ast.NodeHeading == hChild.Type {
347 hChild.HeadingLevel += parentHeadingLevel - embedTopLevel + 1
348 if 6 < hChild.HeadingLevel {
349 hChild.HeadingLevel = 6
350 }
351 }
352 }
353 }
354 }
355
356 mdBuf := &bytes.Buffer{}
357 for _, hChild := range hChildren {
358 md, _ = lute.FormatNodeSync(hChild, luteEngine.ParseOptions, luteEngine.RenderOptions)
359 mdBuf.WriteString(md)
360 mdBuf.WriteString("\n\n")
361 }
362 md = mdBuf.String()
363 } else {
364 node := treenode.GetNodeInTree(subTree, sqlBlock.ID)
365 md, _ = lute.FormatNodeSync(node, luteEngine.ParseOptions, luteEngine.RenderOptions)
366 }
367
368 buf := &bytes.Buffer{}
369 lines := strings.Split(md, "\n")
370 for i, line := range lines {
371 if 0 == blockEmbedMode { // 使用原始文本
372 buf.WriteString(line)
373 } else { // 使用引述块
374 buf.WriteString("> " + line)
375 }
376 if i < len(lines)-1 {
377 buf.WriteString("\n")
378 }
379 }
380 buf.WriteString("\n\n")
381
382 subTree = parse.Parse("", buf.Bytes(), luteEngine.ParseOptions)
383 var inserts []*ast.Node
384 for subNode := subTree.Root.FirstChild; nil != subNode; subNode = subNode.Next {
385 if ast.NodeKramdownBlockIAL != subNode.Type {
386 inserts = append(inserts, subNode)
387 }
388 }
389 if 2 < len(n.KramdownIAL) && 0 < len(inserts) {
390 if bookmark := n.IALAttr("bookmark"); "" != bookmark {
391 inserts[0].SetIALAttr("bookmark", bookmark)
392 }
393 if name := n.IALAttr("name"); "" != name {
394 inserts[0].SetIALAttr("name", name)
395 }
396 if alias := n.IALAttr("alias"); "" != alias {
397 inserts[0].SetIALAttr("alias", alias)
398 }
399 if memo := n.IALAttr("memo"); "" != memo {
400 inserts[0].SetIALAttr("memo", memo)
401 }
402 }
403 for _, insert := range inserts {
404 n.InsertBefore(insert)
405
406 if gulu.Str.Contains(sqlBlock.ID, *resolved) {
407 return ast.WalkContinue
408 }
409
410 resolveEmbedR(insert, blockEmbedMode, luteEngine, resolved, depth)
411 }
412 }
413 unlinks = append(unlinks, n)
414 return ast.WalkSkipChildren
415 }
416 return ast.WalkContinue
417 })
418 for _, unlink := range unlinks {
419 unlink.Unlink()
420 }
421 }
422 return
423}
424
425func renderBlockMarkdownR(id string, rendered *[]string) (ret []*ast.Node) {
426 if gulu.Str.Contains(id, *rendered) {
427 return
428 }
429 *rendered = append(*rendered, id)
430
431 b := treenode.GetBlockTree(id)
432 if nil == b {
433 return
434 }
435
436 var err error
437 var t *parse.Tree
438 if t, err = LoadTreeByBlockID(b.ID); err != nil {
439 return
440 }
441 node := treenode.GetNodeInTree(t, b.ID)
442 if nil == node {
443 return
444 }
445
446 var children []*ast.Node
447 if ast.NodeHeading == node.Type {
448 children = append(children, node)
449 children = append(children, treenode.HeadingChildren(node)...)
450 } else if ast.NodeDocument == node.Type {
451 for c := node.FirstChild; nil != c; c = c.Next {
452 children = append(children, c)
453 }
454 } else {
455 children = append(children, node)
456 }
457
458 for _, child := range children {
459 var unlinks, inserts []*ast.Node
460 ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus {
461 if !entering || !n.IsBlock() {
462 return ast.WalkContinue
463 }
464
465 if ast.NodeBlockQueryEmbed == n.Type {
466 stmt := n.ChildByType(ast.NodeBlockQueryEmbedScript).TokensStr()
467 stmt = html.UnescapeString(stmt)
468 stmt = strings.ReplaceAll(stmt, editor.IALValEscNewLine, "\n")
469 sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, Conf.Search.Limit)
470 for _, sqlBlock := range sqlBlocks {
471 subNodes := renderBlockMarkdownR(sqlBlock.ID, rendered)
472 for _, subNode := range subNodes {
473 inserts = append(inserts, subNode)
474 }
475 }
476 unlinks = append(unlinks, n)
477 return ast.WalkSkipChildren
478 }
479 return ast.WalkContinue
480 })
481 for _, n := range unlinks {
482 n.Unlink()
483 }
484
485 if ast.NodeBlockQueryEmbed != child.Type {
486 ret = append(ret, child)
487 } else {
488 for _, n := range inserts {
489 ret = append(ret, n)
490 }
491 }
492
493 }
494 return
495}