// SiYuan - Refactor your thinking // Copyright (c) 2020-present, b3log.org // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package model import ( "bytes" "github.com/88250/lute/editor" "regexp" "strings" "github.com/88250/gulu" "github.com/88250/lute" "github.com/88250/lute/ast" "github.com/88250/lute/html" "github.com/88250/lute/parse" "github.com/88250/lute/render" "github.com/siyuan-note/siyuan/kernel/sql" "github.com/siyuan-note/siyuan/kernel/treenode" "github.com/siyuan-note/siyuan/kernel/util" ) func renderOutline(heading *ast.Node, luteEngine *lute.Lute) (ret string) { if nil == heading { return "" } if ast.NodeDocument == heading.Type { return heading.IALAttr("title") } buf := bytes.Buffer{} buf.Grow(4096) ast.Walk(heading, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { switch n.Type { case ast.NodeHeading: // Show heading block appearance style in the Outline Panel https://github.com/siyuan-note/siyuan/issues/7872 if style := n.IALAttr("style"); "" != style { buf.WriteString("") } } return ast.WalkContinue } if style := n.IALAttr("style"); "" != style { if strings.Contains(style, "font-size") { // 大纲字号不应该跟随字体设置 https://github.com/siyuan-note/siyuan/issues/7202 style = regexp.MustCompile("font-size:.*?;").ReplaceAllString(style, "font-size: inherit;") n.SetIALAttr("style", style) } } switch n.Type { case ast.NodeHeading: // Show heading block appearance style in the Outline Panel https://github.com/siyuan-note/siyuan/issues/7872 if style := n.IALAttr("style"); "" != style { buf.WriteString("") } case ast.NodeText, ast.NodeLinkText, ast.NodeCodeBlockCode, ast.NodeMathBlockContent: tokens := html.EscapeHTML(n.Tokens) tokens = bytes.ReplaceAll(tokens, []byte(" "), []byte(" ")) // 大纲面板条目中无法显示多个空格 https://github.com/siyuan-note/siyuan/issues/4370 buf.Write(tokens) case ast.NodeBackslashContent: buf.Write(html.EscapeHTML(n.Tokens)) case ast.NodeTextMark: dom := luteEngine.RenderNodeBlockDOM(n) buf.WriteString(dom) return ast.WalkSkipChildren case ast.NodeImage: return ast.WalkSkipChildren } return ast.WalkContinue }) ret = strings.TrimSpace(buf.String()) ret = strings.ReplaceAll(ret, "\n", "") return } func renderBlockText(node *ast.Node, excludeTypes []string) (ret string) { if nil == node { return } ret = sql.NodeStaticContent(node, excludeTypes, false, false, false, GetBlockAttrsWithoutWaitWriting) ret = strings.TrimSpace(ret) ret = strings.ReplaceAll(ret, "\n", "") ret = util.EscapeHTML(ret) ret = strings.TrimSpace(ret) if "" == ret { // 复制内容为空的块作为块引用时粘贴无效 https://github.com/siyuan-note/siyuan/issues/4962 buf := bytes.Buffer{} ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue } if ast.NodeImage == n.Type { title := n.ChildByType(ast.NodeLinkTitle) if nil == title { alt := n.ChildByType(ast.NodeLinkText) if nil != alt && 0 < len(alt.Tokens) { buf.Write(alt.Tokens) } else { buf.WriteString("image") } } else { buf.Write(title.Tokens) } } return ast.WalkContinue }) ret = buf.String() } return } func renderBlockDOMByNodes(nodes []*ast.Node, luteEngine *lute.Lute) string { tree := &parse.Tree{Root: &ast.Node{Type: ast.NodeDocument}, Context: &parse.Context{ParseOption: luteEngine.ParseOptions}} blockRenderer := render.NewProtyleRenderer(tree, luteEngine.RenderOptions) for _, n := range nodes { ast.Walk(n, func(node *ast.Node, entering bool) ast.WalkStatus { rendererFunc := blockRenderer.RendererFuncs[node.Type] return rendererFunc(node, entering) }) } h := strings.TrimSpace(blockRenderer.Writer.String()) if strings.HasPrefix(h, "" } return h } func renderBlockContentByNodes(nodes []*ast.Node) string { var subNodes []*ast.Node for _, n := range nodes { if ast.NodeDocument == n.Type { for c := n.FirstChild; nil != c; c = c.Next { subNodes = append(subNodes, c) } } else { subNodes = append(subNodes, n) } } buf := bytes.Buffer{} for _, n := range subNodes { buf.WriteString(sql.NodeStaticContent(n, nil, false, false, false, GetBlockAttrsWithoutWaitWriting)) } return buf.String() } func resolveEmbedR(n *ast.Node, blockEmbedMode int, luteEngine *lute.Lute, resolved *[]string, depth *int) { var children []*ast.Node if ast.NodeHeading == n.Type { children = append(children, n) children = append(children, treenode.HeadingChildren(n)...) } else if ast.NodeDocument == n.Type { for c := n.FirstChild; nil != c; c = c.Next { children = append(children, c) } } else { children = append(children, n) } *depth++ if 7 < *depth { return } for _, child := range children { var unlinks []*ast.Node parentHeadingLevel := 0 for prev := child; nil != prev; prev = prev.Previous { if ast.NodeHeading == prev.Type { parentHeadingLevel = prev.HeadingLevel break } } ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering || !n.IsBlock() { return ast.WalkContinue } if ast.NodeBlockQueryEmbed == n.Type { if gulu.Str.Contains(n.ID, *resolved) { return ast.WalkContinue } *resolved = append(*resolved, n.ID) stmt := n.ChildByType(ast.NodeBlockQueryEmbedScript).TokensStr() stmt = html.UnescapeString(stmt) stmt = strings.ReplaceAll(stmt, editor.IALValEscNewLine, "\n") sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, Conf.Search.Limit) for _, sqlBlock := range sqlBlocks { if "query_embed" == sqlBlock.Type { continue } subTree, _ := LoadTreeByBlockID(sqlBlock.ID) var md string if "d" == sqlBlock.Type { md, _ = lute.FormatNodeSync(subTree.Root, luteEngine.ParseOptions, luteEngine.RenderOptions) } else if "h" == sqlBlock.Type { h := treenode.GetNodeInTree(subTree, sqlBlock.ID) var hChildren []*ast.Node hChildren = append(hChildren, h) hChildren = append(hChildren, treenode.HeadingChildren(h)...) if 0 == blockEmbedMode { // 嵌入块中出现了大于等于上方非嵌入块的标题时需要降低嵌入块中的标题级别 // Improve export of heading levels in embedded blocks https://github.com/siyuan-note/siyuan/issues/12233 embedTopLevel := 0 for _, hChild := range hChildren { if ast.NodeHeading == hChild.Type { embedTopLevel = hChild.HeadingLevel break } } if parentHeadingLevel >= embedTopLevel { for _, hChild := range hChildren { if ast.NodeHeading == hChild.Type { hChild.HeadingLevel += parentHeadingLevel - embedTopLevel + 1 if 6 < hChild.HeadingLevel { hChild.HeadingLevel = 6 } } } } } mdBuf := &bytes.Buffer{} for _, hChild := range hChildren { md, _ = lute.FormatNodeSync(hChild, luteEngine.ParseOptions, luteEngine.RenderOptions) mdBuf.WriteString(md) mdBuf.WriteString("\n\n") } md = mdBuf.String() } else { node := treenode.GetNodeInTree(subTree, sqlBlock.ID) md, _ = lute.FormatNodeSync(node, luteEngine.ParseOptions, luteEngine.RenderOptions) } buf := &bytes.Buffer{} lines := strings.Split(md, "\n") for i, line := range lines { if 0 == blockEmbedMode { // 使用原始文本 buf.WriteString(line) } else { // 使用引述块 buf.WriteString("> " + line) } if i < len(lines)-1 { buf.WriteString("\n") } } buf.WriteString("\n\n") subTree = parse.Parse("", buf.Bytes(), luteEngine.ParseOptions) var inserts []*ast.Node for subNode := subTree.Root.FirstChild; nil != subNode; subNode = subNode.Next { if ast.NodeKramdownBlockIAL != subNode.Type { inserts = append(inserts, subNode) } } for _, insert := range inserts { n.InsertBefore(insert) if gulu.Str.Contains(sqlBlock.ID, *resolved) { return ast.WalkContinue } resolveEmbedR(insert, blockEmbedMode, luteEngine, resolved, depth) } } unlinks = append(unlinks, n) return ast.WalkSkipChildren } return ast.WalkContinue }) for _, unlink := range unlinks { unlink.Unlink() } } return } func renderBlockMarkdownR(id string, rendered *[]string) (ret []*ast.Node) { if gulu.Str.Contains(id, *rendered) { return } *rendered = append(*rendered, id) b := treenode.GetBlockTree(id) if nil == b { return } var err error var t *parse.Tree if t, err = LoadTreeByBlockID(b.ID); err != nil { return } node := treenode.GetNodeInTree(t, b.ID) if nil == node { return } var children []*ast.Node if ast.NodeHeading == node.Type { children = append(children, node) children = append(children, treenode.HeadingChildren(node)...) } else if ast.NodeDocument == node.Type { for c := node.FirstChild; nil != c; c = c.Next { children = append(children, c) } } else { children = append(children, node) } for _, child := range children { var unlinks, inserts []*ast.Node ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering || !n.IsBlock() { return ast.WalkContinue } if ast.NodeBlockQueryEmbed == n.Type { stmt := n.ChildByType(ast.NodeBlockQueryEmbedScript).TokensStr() stmt = html.UnescapeString(stmt) stmt = strings.ReplaceAll(stmt, editor.IALValEscNewLine, "\n") sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, Conf.Search.Limit) for _, sqlBlock := range sqlBlocks { subNodes := renderBlockMarkdownR(sqlBlock.ID, rendered) for _, subNode := range subNodes { inserts = append(inserts, subNode) } } unlinks = append(unlinks, n) return ast.WalkSkipChildren } return ast.WalkContinue }) for _, n := range unlinks { n.Unlink() } if ast.NodeBlockQueryEmbed != child.Type { ret = append(ret, child) } else { for _, n := range inserts { ret = append(ret, n) } } } return }