diff --git a/kernel/api/block.go b/kernel/api/block.go index 70fe8bcc8..662e60e99 100644 --- a/kernel/api/block.go +++ b/kernel/api/block.go @@ -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) diff --git a/kernel/api/router.go b/kernel/api/router.go index 495b86175..7b5f603d9 100644 --- a/kernel/api/router.go +++ b/kernel/api/router.go @@ -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) diff --git a/kernel/model/block.go b/kernel/model/block.go index 264885959..d3ac541ad 100644 --- a/kernel/model/block.go +++ b/kernel/model/block.go @@ -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 := `