From ca24bea9368b32ec707345d17cb0125a4362c054 Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Fri, 9 Jan 2026 19:11:41 +0800 Subject: [PATCH] :art: Improve handling of assets when exporting to Word .docx format https://github.com/siyuan-note/siyuan/issues/15253 Signed-off-by: Daniel <845765@qq.com> --- kernel/model/assets.go | 44 +++++++++++++++++++++--------------------- kernel/model/export.go | 35 +++++++++++++++++++++------------ kernel/sql/asset.go | 2 +- kernel/sql/database.go | 6 +++--- kernel/util/path.go | 4 ++-- 5 files changed, 51 insertions(+), 40 deletions(-) diff --git a/kernel/model/assets.go b/kernel/model/assets.go index 4f5d0bd6b..8827d1991 100644 --- a/kernel/model/assets.go +++ b/kernel/model/assets.go @@ -177,7 +177,7 @@ func DocAssets(rootID string) (ret []string, err error) { return } - ret = getAssetsLinkDests(tree.Root) + ret = getAssetsLinkDests(tree.Root, false) return } @@ -521,7 +521,7 @@ func UploadAssets2Cloud(id string, ignorePushMsg bool) (count int, err error) { var assets []string for _, n := range nodes { - assets = append(assets, getAssetsLinkDests(n)...) + assets = append(assets, getAssetsLinkDests(n, false)...) assets = append(assets, getQueryEmbedNodesAssetsLinkDests(n)...) } assets = gulu.Str.RemoveDuplicatedElem(assets) @@ -937,13 +937,13 @@ func UnusedAssets() (ret []string) { trees = append(trees, tree) } for _, tree := range trees { - for _, d := range getAssetsLinkDests(tree.Root) { + for _, d := range getAssetsLinkDests(tree.Root, false) { dests[d] = true } if titleImgPath := treenode.GetDocTitleImgPath(tree.Root); "" != titleImgPath { // 题头图计入 - if !util.IsAssetLinkDest([]byte(titleImgPath)) { + if !util.IsAssetLinkDest([]byte(titleImgPath), false) { continue } dests[titleImgPath] = true @@ -1100,13 +1100,13 @@ func MissingAssets() (ret []string) { trees = append(trees, tree) } for _, tree := range trees { - for _, d := range getAssetsLinkDests(tree.Root) { + for _, d := range getAssetsLinkDests(tree.Root, false) { dests[d] = true } if titleImgPath := treenode.GetDocTitleImgPath(tree.Root); "" != titleImgPath { // 题头图计入 - if !util.IsAssetLinkDest([]byte(titleImgPath)) { + if !util.IsAssetLinkDest([]byte(titleImgPath), false) { continue } dests[titleImgPath] = true @@ -1204,7 +1204,7 @@ func getQueryEmbedNodesAssetsLinkDests(node *ast.Node) (ret []string) { continue } - ret = append(ret, getAssetsLinkDests(embedNode)...) + ret = append(ret, getAssetsLinkDests(embedNode, false)...) } return ast.WalkContinue }) @@ -1212,7 +1212,7 @@ func getQueryEmbedNodesAssetsLinkDests(node *ast.Node) (ret []string) { return } -func getAssetsLinkDests(node *ast.Node) (ret []string) { +func getAssetsLinkDests(node *ast.Node, includePublic bool) (ret []string) { ret = []string{} ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus { if n.IsBlock() { @@ -1222,7 +1222,7 @@ func getAssetsLinkDests(node *ast.Node) (ret []string) { k := kv[0] if strings.HasPrefix(k, "custom-data-assets") { dest := kv[1] - if "" == dest || !util.IsAssetLinkDest([]byte(dest)) { + if "" == dest || !util.IsAssetLinkDest([]byte(dest), includePublic) { continue } ret = append(ret, dest) @@ -1238,21 +1238,21 @@ func getAssetsLinkDests(node *ast.Node) (ret []string) { } if ast.NodeLinkDest == n.Type { - if !util.IsAssetLinkDest(n.Tokens) { + if !util.IsAssetLinkDest(n.Tokens, includePublic) { return ast.WalkContinue } dest := strings.TrimSpace(string(n.Tokens)) ret = append(ret, dest) } else if n.IsTextMarkType("a") { - if !util.IsAssetLinkDest(gulu.Str.ToBytes(n.TextMarkAHref)) { + if !util.IsAssetLinkDest(gulu.Str.ToBytes(n.TextMarkAHref), includePublic) { return ast.WalkContinue } dest := strings.TrimSpace(n.TextMarkAHref) ret = append(ret, dest) } else if n.IsTextMarkType("file-annotation-ref") { - if !util.IsAssetLinkDest(gulu.Str.ToBytes(n.TextMarkFileAnnotationRefID)) { + if !util.IsAssetLinkDest(gulu.Str.ToBytes(n.TextMarkFileAnnotationRefID), includePublic) { return ast.WalkContinue } @@ -1278,7 +1278,7 @@ func getAssetsLinkDests(node *ast.Node) (ret []string) { for _, asset := range value.MAsset { dest := asset.Content - if !util.IsAssetLinkDest([]byte(dest)) { + if !util.IsAssetLinkDest([]byte(dest), includePublic) { continue } ret = append(ret, strings.TrimSpace(dest)) @@ -1288,7 +1288,7 @@ func getAssetsLinkDests(node *ast.Node) (ret []string) { for _, value := range keyValues.Values { if nil != value.URL { dest := value.URL.Content - if !util.IsAssetLinkDest([]byte(dest)) { + if !util.IsAssetLinkDest([]byte(dest), includePublic) { continue } ret = append(ret, strings.TrimSpace(dest)) @@ -1303,13 +1303,13 @@ func getAssetsLinkDests(node *ast.Node) (ret []string) { // 兼容两种属性名 custom-data-assets 和 data-assets https://github.com/siyuan-note/siyuan/issues/4122#issuecomment-1154796568 dataAssets = n.IALAttr("data-assets") } - if !util.IsAssetLinkDest([]byte(dataAssets)) { + if !util.IsAssetLinkDest([]byte(dataAssets), includePublic) { return ast.WalkContinue } ret = append(ret, dataAssets) } else { // HTMLBlock/InlineHTML/IFrame/Audio/Video dest := treenode.GetNodeSrcTokens(n) - if !util.IsAssetLinkDest([]byte(dest)) { + if !util.IsAssetLinkDest([]byte(dest), includePublic) { return ast.WalkContinue } ret = append(ret, dest) @@ -1379,7 +1379,7 @@ func getRemoteAssetsLinkDests(node *ast.Node, onlyImg bool) (ret []string) { if onlyImg { if ast.NodeLinkDest == node.Type { if node.ParentIs(ast.NodeImage) { - if !util.IsAssetLinkDest(node.Tokens) { + if !util.IsAssetLinkDest(node.Tokens, false) { ret = append(ret, string(node.Tokens)) } @@ -1406,7 +1406,7 @@ func getRemoteAssetsLinkDests(node *ast.Node, onlyImg bool) (ret []string) { } dest := asset.Content - if !util.IsAssetLinkDest([]byte(dest)) { + if !util.IsAssetLinkDest([]byte(dest), false) { ret = append(ret, strings.TrimSpace(dest)) } } @@ -1415,16 +1415,16 @@ func getRemoteAssetsLinkDests(node *ast.Node, onlyImg bool) (ret []string) { } } else { if ast.NodeLinkDest == node.Type { - if !util.IsAssetLinkDest(node.Tokens) { + if !util.IsAssetLinkDest(node.Tokens, false) { ret = append(ret, string(node.Tokens)) } } else if node.IsTextMarkType("a") { - if !util.IsAssetLinkDest([]byte(node.TextMarkAHref)) { + if !util.IsAssetLinkDest([]byte(node.TextMarkAHref), false) { ret = append(ret, node.TextMarkAHref) } } else if ast.NodeAudio == node.Type || ast.NodeVideo == node.Type { src := treenode.GetNodeSrcTokens(node) - if !util.IsAssetLinkDest([]byte(src)) { + if !util.IsAssetLinkDest([]byte(src), false) { ret = append(ret, src) } } else if ast.NodeAttributeView == node.Type { @@ -1445,7 +1445,7 @@ func getRemoteAssetsLinkDests(node *ast.Node, onlyImg bool) (ret []string) { for _, asset := range value.MAsset { dest := asset.Content - if !util.IsAssetLinkDest([]byte(dest)) { + if !util.IsAssetLinkDest([]byte(dest), false) { ret = append(ret, strings.TrimSpace(dest)) } } diff --git a/kernel/model/export.go b/kernel/model/export.go index 2d910e7fb..9dcd58b10 100644 --- a/kernel/model/export.go +++ b/kernel/model/export.go @@ -276,7 +276,7 @@ func Export2Liandi(id string) (err error) { return errors.New(Conf.Language(204)) } - assets := getAssetsLinkDests(tree.Root) + assets := getAssetsLinkDests(tree.Root, false) embedAssets := getQueryEmbedNodesAssetsLinkDests(tree.Root) assets = append(assets, embedAssets...) assets = gulu.Str.RemoveDuplicatedElem(assets) @@ -795,9 +795,20 @@ func ExportMarkdownHTML(id, savePath string, docx, merge bool) (name, dom string return } - assets := getAssetsLinkDests(tree.Root) + if docx { + ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { + if ast.NodeLinkDest == n.Type { + if bytes.HasPrefix(n.Tokens, []byte("file://")) { + n.Tokens = bytes.ReplaceAll(n.Tokens, []byte("\\"), []byte("/")) + } + } + return ast.WalkContinue + }) + } + + assets := getAssetsLinkDests(tree.Root, docx) for _, asset := range assets { - if strings.HasPrefix(asset, "assets/") { + if strings.HasPrefix(asset, "assets/") || strings.HasPrefix(asset, "public/") { if strings.Contains(asset, "?") { asset = asset[:strings.LastIndex(asset, "?")] } @@ -991,7 +1002,7 @@ func ExportHTML(id, savePath string, pdf, image, keepFold, merge bool) (name, do return } - assets := getAssetsLinkDests(tree.Root) + assets := getAssetsLinkDests(tree.Root, false) for _, asset := range assets { if strings.Contains(asset, "?") { asset = asset[:strings.LastIndex(asset, "?")] @@ -1174,7 +1185,7 @@ func ProcessPDF(id, p string, merge, removeAssets, watermark bool) (err error) { } var headings []*ast.Node - assetDests := getAssetsLinkDests(tree.Root) + assetDests := getAssetsLinkDests(tree.Root, false) ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue @@ -1874,10 +1885,10 @@ func exportSYZip(boxID, rootDirPath, baseFolderName string, docPaths []string) ( copiedAssets := hashset.New() for _, tree := range trees { var assets []string - assets = append(assets, getAssetsLinkDests(tree.Root)...) + assets = append(assets, getAssetsLinkDests(tree.Root, false)...) titleImgPath := treenode.GetDocTitleImgPath(tree.Root) // Export .sy.zip doc title image is not exported https://github.com/siyuan-note/siyuan/issues/8748 if "" != titleImgPath { - if util.IsAssetLinkDest([]byte(titleImgPath)) { + if util.IsAssetLinkDest([]byte(titleImgPath), false) { assets = append(assets, titleImgPath) } } @@ -2073,7 +2084,7 @@ func exportAv(avID, exportStorageAvDir, exportFolder string, assetPathMap map[st case av.KeyTypeMAsset: // 导出资源文件列 https://github.com/siyuan-note/siyuan/issues/9919 for _, value := range keyValues.Values { for _, asset := range value.MAsset { - if !util.IsAssetLinkDest([]byte(asset.Content)) { + if !util.IsAssetLinkDest([]byte(asset.Content), false) { continue } @@ -2218,17 +2229,17 @@ func exportMarkdownContent0(id string, tree *parse.Tree, cloudAssetsBase string, } if ast.NodeLinkDest == n.Type { - if util.IsAssetLinkDest(n.Tokens) { + if util.IsAssetLinkDest(n.Tokens, false) { n.Tokens = bytes.ReplaceAll(n.Tokens, []byte(" "), []byte("_")) } } else if n.IsTextMarkType("a") { href := n.TextMarkAHref - if util.IsAssetLinkDest([]byte(href)) { + if util.IsAssetLinkDest([]byte(href), false) { n.TextMarkAHref = strings.ReplaceAll(href, " ", "_") } } else if ast.NodeIFrame == n.Type || ast.NodeAudio == n.Type || ast.NodeVideo == n.Type { dest := treenode.GetNodeSrcTokens(n) - if util.IsAssetLinkDest([]byte(dest)) { + if util.IsAssetLinkDest([]byte(dest), false) { setAssetsLinkDest(n, dest, strings.ReplaceAll(dest, " ", "_")) } } @@ -3353,7 +3364,7 @@ func exportPandocConvertZip(baseFolderName string, docPaths, defBlockIDs []strin // 解析导出后的标准 Markdown,汇总 assets tree = parse.Parse("", gulu.Str.ToBytes(md), luteEngine.ParseOptions) var assets []string - assets = append(assets, getAssetsLinkDests(tree.Root)...) + assets = append(assets, getAssetsLinkDests(tree.Root, false)...) for _, asset := range assets { asset = string(html.DecodeDestination([]byte(asset))) if strings.Contains(asset, "?") { diff --git a/kernel/sql/asset.go b/kernel/sql/asset.go index ad58dd3d0..8819b756f 100644 --- a/kernel/sql/asset.go +++ b/kernel/sql/asset.go @@ -65,7 +65,7 @@ func docTagSpans(n *ast.Node) (ret []*Span) { func docTitleImgAsset(root *ast.Node, boxLocalPath, docDirLocalPath string) *Asset { if p := treenode.GetDocTitleImgPath(root); "" != p { - if !util.IsAssetLinkDest([]byte(p)) { + if !util.IsAssetLinkDest([]byte(p), false) { return nil } diff --git a/kernel/sql/database.go b/kernel/sql/database.go index dad061965..07e7ca33a 100644 --- a/kernel/sql/database.go +++ b/kernel/sql/database.go @@ -631,7 +631,7 @@ func buildSpanFromNode(n *ast.Node, tree *parse.Tree, rootID, boxID, p string) ( // assetsLinkDestsInTree - if !util.IsAssetLinkDest(destNode.Tokens) { + if !util.IsAssetLinkDest(destNode.Tokens, false) { return } @@ -686,7 +686,7 @@ func buildSpanFromNode(n *ast.Node, tree *parse.Tree, rootID, boxID, p string) ( if n.IsTextMarkType("a") { dest := n.TextMarkAHref - if util.IsAssetLinkDest([]byte(dest)) { + if util.IsAssetLinkDest([]byte(dest), false) { var title string if titleNode := n.ChildByType(ast.NodeLinkTitle); nil != titleNode { title = gulu.Str.FromBytes(titleNode.Tokens) @@ -776,7 +776,7 @@ func buildSpanFromNode(n *ast.Node, tree *parse.Tree, rootID, boxID, p string) ( return } - if !util.IsAssetLinkDest(src) { + if !util.IsAssetLinkDest(src, false) { walkStatus = ast.WalkContinue return } diff --git a/kernel/util/path.go b/kernel/util/path.go index e2f39a6ef..b11d3dcab 100644 --- a/kernel/util/path.go +++ b/kernel/util/path.go @@ -316,8 +316,8 @@ func FilterSelfChildDocs(paths []string) (ret []string) { return } -func IsAssetLinkDest(dest []byte) bool { - return bytes.HasPrefix(dest, []byte("assets/")) +func IsAssetLinkDest(dest []byte, includePublic bool) bool { + return bytes.HasPrefix(dest, []byte("assets/")) || (includePublic && bytes.HasPrefix(dest, []byte("public/"))) } var (