🧑‍💻 Add internal kernel API /api/block/getBlockDOMWithEmbed and /api/block/getBlockDOMsWithEmbed (#16237)

* 🎨 Add internal kernel API `/api/block/getBlockDOMWithEmbed` and `/api/block/getBlockDOMsWithEmbed`

fix https://github.com/siyuan-note/siyuan/issues/16021

* 🎨 Add internal kernel API `/api/block/getBlockDOMWithEmbed` and `/api/block/getBlockDOMsWithEmbed`

fix https://github.com/siyuan-note/siyuan/issues/16021
This commit is contained in:
Jeffrey Chen 2025-10-29 15:27:19 +08:00 committed by GitHub
parent 65c8344add
commit 4c2684d11d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 190 additions and 0 deletions

View file

@ -738,6 +738,42 @@ func getBlockDOMs(c *gin.Context) {
ret.Data = doms
}
func getBlockDOMWithEmbed(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
dom := model.GetBlockDOMWithEmbed(id)
ret.Data = map[string]string{
"id": id,
"dom": dom,
}
}
func getBlockDOMsWithEmbed(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
idsArg := arg["ids"].([]interface{})
var ids []string
for _, id := range idsArg {
ids = append(ids, id.(string))
}
doms := model.GetBlockDOMsWithEmbed(ids)
ret.Data = doms
}
func getBlockKramdown(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)

View file

@ -185,6 +185,8 @@ func ServeAPI(ginServer *gin.Engine) {
ginServer.Handle("POST", "/api/block/getBlockInfo", model.CheckAuth, getBlockInfo)
ginServer.Handle("POST", "/api/block/getBlockDOM", model.CheckAuth, getBlockDOM)
ginServer.Handle("POST", "/api/block/getBlockDOMs", model.CheckAuth, getBlockDOMs)
ginServer.Handle("POST", "/api/block/getBlockDOMWithEmbed", model.CheckAuth, getBlockDOMWithEmbed)
ginServer.Handle("POST", "/api/block/getBlockDOMsWithEmbed", model.CheckAuth, getBlockDOMsWithEmbed)
ginServer.Handle("POST", "/api/block/getBlockKramdown", model.CheckAuth, getBlockKramdown)
ginServer.Handle("POST", "/api/block/getChildBlocks", model.CheckAuth, getChildBlocks)
ginServer.Handle("POST", "/api/block/getTailChildBlocks", model.CheckAuth, getTailChildBlocks)

View file

@ -20,12 +20,16 @@ import (
"bytes"
"errors"
"fmt"
"html"
"regexp"
"slices"
"strings"
"time"
"github.com/88250/gulu"
"github.com/88250/lute"
"github.com/88250/lute/ast"
"github.com/88250/lute/editor"
"github.com/88250/lute/parse"
"github.com/88250/lute/render"
"github.com/open-spaced-repetition/go-fsrs/v3"
@ -804,6 +808,154 @@ func GetBlockDOMs(ids []string) (ret map[string]string) {
return
}
func GetBlockDOMWithEmbed(id string) (ret string) {
if "" == id {
return
}
doms := GetBlockDOMsWithEmbed([]string{id})
ret = doms[id]
return
}
func GetBlockDOMsWithEmbed(ids []string) (ret map[string]string) {
ret = map[string]string{}
if 0 == len(ids) {
return
}
luteEngine := NewLute()
trees := filesys.LoadTrees(ids)
for id, tree := range trees {
node := treenode.GetNodeInTree(tree, id)
if nil == node {
continue
}
resolveEmbedContent(node, luteEngine)
// 处理折叠标题
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || !n.IsBlock() {
return ast.WalkContinue
}
if parentFoldedHeading := treenode.GetParentFoldedHeading(n); nil != parentFoldedHeading {
n.SetIALAttr("parent-heading", parentFoldedHeading.ID)
}
return ast.WalkContinue
})
htmlContent := luteEngine.RenderNodeBlockDOM(node)
htmlContent = processEmbedHTML(htmlContent)
ret[id] = htmlContent
}
return
}
func resolveEmbedContent(n *ast.Node, luteEngine *lute.Lute) {
ast.Walk(n, func(node *ast.Node, entering bool) ast.WalkStatus {
if !entering || ast.NodeBlockQueryEmbed != node.Type {
return ast.WalkContinue
}
// 获取嵌入块的查询语句
scriptNode := node.ChildByType(ast.NodeBlockQueryEmbedScript)
if nil == scriptNode {
return ast.WalkContinue
}
stmt := scriptNode.TokensStr()
stmt = html.UnescapeString(stmt)
stmt = strings.ReplaceAll(stmt, editor.IALValEscNewLine, "\n")
// 执行查询获取嵌入的块
sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, Conf.Search.Limit)
// 收集所有嵌入块的内容 HTML
var embedContents []string
for _, sqlBlock := range sqlBlocks {
if "query_embed" == sqlBlock.Type {
continue
}
subTree, _ := LoadTreeByBlockID(sqlBlock.ID)
if nil == subTree {
continue
}
// 将内容转换为 HTML直接使用原始 AST 节点渲染以保持正确的 data-node-id
var contentHTML string
if "d" == sqlBlock.Type {
// 文档块:直接使用原始 AST 节点渲染,保持原始的 data-node-id
contentHTML = luteEngine.RenderNodeBlockDOM(subTree.Root)
} else if "h" == sqlBlock.Type {
// 标题块:使用标题及其子块的原始 AST 节点渲染
h := treenode.GetNodeInTree(subTree, sqlBlock.ID)
if nil == h {
continue
}
var hChildren []*ast.Node
hChildren = append(hChildren, h)
hChildren = append(hChildren, treenode.HeadingChildren(h)...)
// 创建一个临时的文档节点来包含所有子节点
tempRoot := &ast.Node{Type: ast.NodeDocument}
for _, hChild := range hChildren {
tempRoot.AppendChild(hChild)
}
contentHTML = luteEngine.RenderNodeBlockDOM(tempRoot)
} else {
// 其他块:直接使用原始 AST 节点渲染
blockNode := treenode.GetNodeInTree(subTree, sqlBlock.ID)
if nil == blockNode {
continue
}
contentHTML = luteEngine.RenderNodeBlockDOM(blockNode)
}
if contentHTML != "" {
embedContents = append(embedContents, contentHTML)
}
}
// 如果有内容,在嵌入块上添加内容标记
if len(embedContents) > 0 {
node.SetIALAttr("embed-content", strings.Join(embedContents, ""))
}
return ast.WalkContinue
})
}
func processEmbedHTML(htmlStr string) string {
// 使用正则表达式查找所有带有 embed-content 属性的嵌入块
embedPattern := `<div[^>]*data-type="NodeBlockQueryEmbed"[^>]*embed-content="[^"]*"[^>]*>`
re := regexp.MustCompile(embedPattern)
return re.ReplaceAllStringFunc(htmlStr, func(match string) string {
// 提取 embed-content 属性值
contentPattern := `embed-content="([^"]*)"`
contentRe := regexp.MustCompile(contentPattern)
contentMatches := contentRe.FindStringSubmatch(match)
if len(contentMatches) > 1 {
embedContent := contentMatches[1]
// HTML 解码
embedContent = html.UnescapeString(embedContent)
// 移除 embed-content 属性,避免在最终 HTML 中显示
cleanMatch := contentRe.ReplaceAllString(match, "")
// 将内容插入到嵌入块内部
return cleanMatch + embedContent + "</div>"
}
return match
})
}
func GetBlockKramdown(id, mode string) (ret string) {
if "" == id {
return