A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 495 lines 15 kB view raw
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("&nbsp;")) // 大纲面板条目中无法显示多个空格 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}