siyuan/kernel/model/export.go

1582 lines
48 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SiYuan - Build Your Eternal Digital Garden
// 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 <https://www.gnu.org/licenses/>.
package model
import (
"bytes"
"errors"
"fmt"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"unicode/utf8"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/editor"
"github.com/88250/lute/html"
"github.com/88250/lute/parse"
"github.com/88250/lute/render"
"github.com/88250/pdfcpu/pkg/api"
"github.com/88250/pdfcpu/pkg/pdfcpu"
"github.com/emirpasic/gods/sets/hashset"
"github.com/emirpasic/gods/stacks/linkedliststack"
"github.com/siyuan-note/filelock"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func ExportSystemLog() (zipPath string) {
exportFolder := filepath.Join(util.TempDir, "export", "system-log")
os.RemoveAll(exportFolder)
if err := os.MkdirAll(exportFolder, 0755); nil != err {
logging.LogErrorf("create export temp folder failed: %s", err)
return
}
appLog := filepath.Join(util.HomeDir, ".config", "siyuan", "app.log")
if gulu.File.IsExist(appLog) {
to := filepath.Join(exportFolder, "app.log")
if err := gulu.File.CopyFile(appLog, to); nil != err {
logging.LogErrorf("copy app log from [%s] to [%s] failed: %s", err, appLog, to)
}
}
kernelLog := filepath.Join(util.TempDir, "siyuan.log")
if gulu.File.IsExist(kernelLog) {
to := filepath.Join(exportFolder, "siyuan.log")
if err := gulu.File.CopyFile(kernelLog, to); nil != err {
logging.LogErrorf("copy kernel log from [%s] to [%s] failed: %s", err, kernelLog, to)
}
}
zipPath = exportFolder + ".zip"
zip, err := gulu.Zip.Create(zipPath)
if nil != err {
logging.LogErrorf("create export log zip [%s] failed: %s", exportFolder, err)
return ""
}
if err = zip.AddDirectory("log", exportFolder); nil != err {
logging.LogErrorf("create export log zip [%s] failed: %s", exportFolder, err)
return ""
}
if err = zip.Close(); nil != err {
logging.LogErrorf("close export log zip failed: %s", err)
}
os.RemoveAll(exportFolder)
zipPath = "/export/" + url.PathEscape(filepath.Base(zipPath))
return
}
func ExportNotebookSY(id string) (zipPath string) {
zipPath = exportBoxSYZip(id)
return
}
func ExportSY(id string) (name, zipPath string) {
block := treenode.GetBlockTree(id)
if nil == block {
logging.LogErrorf("not found block [%s]", id)
return
}
boxID := block.BoxID
box := Conf.Box(boxID)
baseFolderName := path.Base(block.HPath)
if "." == baseFolderName {
baseFolderName = path.Base(block.Path)
}
rootPath := block.Path
docPaths := []string{rootPath}
docFiles := box.ListFiles(strings.TrimSuffix(block.Path, ".sy"))
for _, docFile := range docFiles {
docPaths = append(docPaths, docFile.path)
}
zipPath = exportSYZip(boxID, path.Dir(rootPath), baseFolderName, docPaths)
name = strings.TrimSuffix(filepath.Base(block.Path), ".sy")
return
}
func ExportDataInFolder(exportFolder string) (err error) {
util.PushEndlessProgress(Conf.Language(65))
defer util.ClearPushProgress(100)
WaitForWritingFiles()
exportFolder = filepath.Join(exportFolder, util.CurrentTimeSecondsStr())
err = exportData(exportFolder)
if nil != err {
return
}
return
}
func ExportData() (zipPath string) {
util.PushEndlessProgress(Conf.Language(65))
defer util.ClearPushProgress(100)
WaitForWritingFiles()
baseFolderName := "data-" + util.CurrentTimeSecondsStr()
exportFolder := filepath.Join(util.TempDir, "export", baseFolderName)
zipPath = exportFolder + ".zip"
err := exportData(exportFolder)
if nil != err {
return
}
zipPath = "/export/" + url.PathEscape(filepath.Base(zipPath))
return
}
func exportData(exportFolder string) (err error) {
baseFolderName := "data-" + util.CurrentTimeSecondsStr()
if err = os.MkdirAll(exportFolder, 0755); nil != err {
logging.LogErrorf("create export temp folder failed: %s", err)
return
}
data := filepath.Join(util.WorkspaceDir, "data")
if err = filelock.RoboCopy(data, exportFolder); nil != err {
logging.LogErrorf("copy data dir from [%s] to [%s] failed: %s", data, baseFolderName, err)
err = errors.New(fmt.Sprintf(Conf.Language(14), formatErrorMsg(err)))
return
}
zipPath := exportFolder + ".zip"
zip, err := gulu.Zip.Create(zipPath)
if nil != err {
logging.LogErrorf("create export data zip [%s] failed: %s", exportFolder, err)
return
}
if err = zip.AddDirectory(baseFolderName, exportFolder); nil != err {
logging.LogErrorf("create export data zip [%s] failed: %s", exportFolder, err)
return
}
if err = zip.Close(); nil != err {
logging.LogErrorf("close export data zip failed: %s", err)
}
os.RemoveAll(exportFolder)
return
}
func Preview(id string) string {
tree, _ := loadTreeByBlockID(id)
tree = exportTree(tree, false, false, false)
luteEngine := NewLute()
luteEngine.SetFootnotes(true)
md := treenode.FormatNode(tree.Root, luteEngine)
tree = parse.Parse("", []byte(md), luteEngine.ParseOptions)
return luteEngine.ProtylePreview(tree, luteEngine.RenderOptions)
}
func ExportDocx(id, savePath string, removeAssets bool) (err error) {
if !util.IsValidPandocBin(Conf.Export.PandocBin) {
return errors.New(Conf.Language(115))
}
tmpDir := filepath.Join(util.TempDir, "export", gulu.Rand.String(7))
if err = os.MkdirAll(tmpDir, 0755); nil != err {
return
}
defer os.Remove(tmpDir)
name, content := ExportMarkdownHTML(id, tmpDir, true)
tmpDocxPath := filepath.Join(tmpDir, name+".docx")
args := []string{ // pandoc -f html --resource-path=请从这里开始 请从这里开始\index.html -o test.docx
"-f", "html+tex_math_dollars",
"--resource-path", tmpDir,
"-o", tmpDocxPath,
}
pandoc := exec.Command(Conf.Export.PandocBin, args...)
gulu.CmdAttr(pandoc)
pandoc.Stdin = bytes.NewBufferString(content)
output, err := pandoc.CombinedOutput()
if nil != err {
logging.LogErrorf("export docx failed: %s", gulu.Str.FromBytes(output))
msg := fmt.Sprintf(Conf.Language(14), gulu.Str.FromBytes(output))
return errors.New(msg)
}
if err = gulu.File.Copy(tmpDocxPath, filepath.Join(savePath, name+".docx")); nil != err {
logging.LogErrorf("export docx failed: %s", err)
return errors.New(fmt.Sprintf(Conf.Language(14), err))
}
if tmpAssets := filepath.Join(tmpDir, "assets"); !removeAssets && gulu.File.IsDir(tmpAssets) {
if err = gulu.File.Copy(tmpAssets, filepath.Join(savePath, "assets")); nil != err {
logging.LogErrorf("export docx failed: %s", err)
return errors.New(fmt.Sprintf(Conf.Language(14), err))
}
}
return
}
func ExportMarkdownHTML(id, savePath string, docx bool) (name, dom string) {
tree, _ := loadTreeByBlockID(id)
tree = exportTree(tree, true, true, false)
name = path.Base(tree.HPath)
name = util.FilterFileName(name) // 导出 PDF、HTML 和 Word 时未移除不支持的文件名符号 https://github.com/siyuan-note/siyuan/issues/5614
if err := os.MkdirAll(savePath, 0755); nil != err {
logging.LogErrorf("mkdir [%s] failed: %s", savePath, err)
return
}
assets := assetsLinkDestsInTree(tree)
for _, asset := range assets {
if strings.HasPrefix(asset, "assets/") {
srcAbsPath, err := GetAssetAbsPath(asset)
if nil != err {
logging.LogWarnf("resolve path of asset [%s] failed: %s", asset, err)
continue
}
targetAbsPath := filepath.Join(savePath, asset)
if err = gulu.File.Copy(srcAbsPath, targetAbsPath); nil != err {
logging.LogWarnf("copy asset from [%s] to [%s] failed: %s", srcAbsPath, targetAbsPath, err)
}
}
}
srcs := []string{"stage/build/export", "stage/build/fonts", "stage/protyle"}
for _, src := range srcs {
from := filepath.Join(util.WorkingDir, src)
to := filepath.Join(savePath, src)
if err := gulu.File.Copy(from, to); nil != err {
logging.LogWarnf("copy stage from [%s] to [%s] failed: %s", from, savePath, err)
return
}
}
theme := Conf.Appearance.ThemeLight
if 1 == Conf.Appearance.Mode {
theme = Conf.Appearance.ThemeDark
}
srcs = []string{"icons", "themes/" + theme}
for _, src := range srcs {
from := filepath.Join(util.AppearancePath, src)
to := filepath.Join(savePath, "appearance", src)
if err := gulu.File.Copy(from, to); nil != err {
logging.LogErrorf("copy appearance from [%s] to [%s] failed: %s", from, savePath, err)
return
}
}
// 复制自定义表情图片
emojis := emojisInTree(tree)
for _, emoji := range emojis {
from := filepath.Join(util.DataDir, emoji)
to := filepath.Join(savePath, emoji)
if err := gulu.File.Copy(from, to); nil != err {
logging.LogErrorf("copy emojis from [%s] to [%s] failed: %s", from, savePath, err)
return
}
}
luteEngine := NewLute()
luteEngine.SetFootnotes(true)
md := treenode.FormatNode(tree.Root, luteEngine)
tree = parse.Parse("", []byte(md), luteEngine.ParseOptions)
if docx {
processIFrame(tree)
}
// 自定义表情图片地址去掉开头的 /
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeEmojiImg == n.Type {
n.Tokens = bytes.ReplaceAll(n.Tokens, []byte("src=\"/emojis"), []byte("src=\"emojis"))
}
return ast.WalkContinue
})
if docx {
renderer := render.NewProtyleExportDocxRenderer(tree, luteEngine.RenderOptions)
output := renderer.Render()
dom = gulu.Str.FromBytes(output)
} else {
dom = luteEngine.ProtylePreview(tree, luteEngine.RenderOptions)
}
return
}
func ExportHTML(id, savePath string, pdf, keepFold bool) (name, dom string) {
tree, _ := loadTreeByBlockID(id)
var headings []*ast.Node
if pdf { // 导出 PDF 需要标记目录书签
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if entering && ast.NodeHeading == n.Type && !n.ParentIs(ast.NodeBlockquote) {
headings = append(headings, n)
return ast.WalkSkipChildren
}
return ast.WalkContinue
})
for _, h := range headings {
link := &ast.Node{Type: ast.NodeLink}
link.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
link.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(" ")})
link.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
link.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
link.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte("pdf-outline://" + h.ID)})
link.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
h.PrependChild(link)
}
}
tree = exportTree(tree, true, true, keepFold)
//if pdf { // TODO: 导出 PDF 时块引转换脚注使用书签跳转 https://github.com/siyuan-note/siyuan/issues/5761
// var footnotesDefs []*ast.Node
// ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
// if entering && ast.NodeFootnotesDef == n.Type {
// footnotesDefs = append(footnotesDefs, n)
// }
// return ast.WalkContinue
// })
// for _, f := range footnotesDefs {
// link := &ast.Node{Type: ast.NodeLink}
// link.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
// link.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(" ")})
// link.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
// link.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
// link.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte("pdf-outline://footnotes-def-" + f.FootnotesRefId)})
// link.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
// f.PrependChild(link)
// }
//}
name = path.Base(tree.HPath)
name = util.FilterFileName(name) // 导出 PDF、HTML 和 Word 时未移除不支持的文件名符号 https://github.com/siyuan-note/siyuan/issues/5614
if "" != savePath {
if err := os.MkdirAll(savePath, 0755); nil != err {
logging.LogErrorf("mkdir [%s] failed: %s", savePath, err)
return
}
assets := assetsLinkDestsInTree(tree)
for _, asset := range assets {
if strings.Contains(asset, "?") {
asset = asset[:strings.LastIndex(asset, "?")]
}
srcAbsPath, err := GetAssetAbsPath(asset)
if nil != err {
logging.LogWarnf("resolve path of asset [%s] failed: %s", asset, err)
continue
}
targetAbsPath := filepath.Join(savePath, asset)
if err = gulu.File.Copy(srcAbsPath, targetAbsPath); nil != err {
logging.LogWarnf("copy asset from [%s] to [%s] failed: %s", srcAbsPath, targetAbsPath, err)
}
}
}
luteEngine := NewLute()
if !pdf && "" != savePath { // 导出 HTML 需要复制静态资源
srcs := []string{"stage/build/export", "stage/build/fonts", "stage/protyle"}
for _, src := range srcs {
from := filepath.Join(util.WorkingDir, src)
to := filepath.Join(savePath, src)
if err := gulu.File.Copy(from, to); nil != err {
logging.LogErrorf("copy stage from [%s] to [%s] failed: %s", from, savePath, err)
return
}
}
theme := Conf.Appearance.ThemeLight
if 1 == Conf.Appearance.Mode {
theme = Conf.Appearance.ThemeDark
}
srcs = []string{"icons", "themes/" + theme}
for _, src := range srcs {
from := filepath.Join(util.AppearancePath, src)
to := filepath.Join(savePath, "appearance", src)
if err := gulu.File.Copy(from, to); nil != err {
logging.LogErrorf("copy appearance from [%s] to [%s] failed: %s", from, savePath, err)
return
}
}
// 复制自定义表情图片
emojis := emojisInTree(tree)
for _, emoji := range emojis {
from := filepath.Join(util.DataDir, emoji)
to := filepath.Join(savePath, emoji)
if err := gulu.File.Copy(from, to); nil != err {
logging.LogErrorf("copy emojis from [%s] to [%s] failed: %s", from, savePath, err)
return
}
}
} else { // 导出 PDF 需要将资源文件路径改为 HTTP 伺服
luteEngine.RenderOptions.LinkBase = "http://127.0.0.1:" + util.ServerPort + "/"
}
if pdf {
processIFrame(tree)
}
luteEngine.SetFootnotes(true)
luteEngine.RenderOptions.ProtyleContenteditable = false
luteEngine.SetProtyleMarkNetImg(false)
renderer := render.NewProtyleExportRenderer(tree, luteEngine.RenderOptions)
dom = gulu.Str.FromBytes(renderer.Render())
return
}
func processIFrame(tree *parse.Tree) {
// 导出 PDF/Word 时 IFrame 块使用超链接 https://github.com/siyuan-note/siyuan/issues/4035
var unlinks []*ast.Node
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeIFrame == n.Type {
index := bytes.Index(n.Tokens, []byte("src=\""))
if 0 > index {
n.InsertBefore(&ast.Node{Type: ast.NodeText, Tokens: n.Tokens})
} else {
src := n.Tokens[index+len("src=\""):]
src = src[:bytes.Index(src, []byte("\""))]
src = html.UnescapeHTML(src)
link := &ast.Node{Type: ast.NodeLink}
link.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
link.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: src})
link.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
link.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
link.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: src})
link.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
n.InsertBefore(link)
}
unlinks = append(unlinks, n)
}
return ast.WalkContinue
})
for _, n := range unlinks {
n.Unlink()
}
}
func AddPDFOutline(id, p string) (err error) {
inFile := p
links, err := api.ListToCLinks(inFile)
if nil != err {
return
}
sort.Slice(links, func(i, j int) bool {
return links[i].Page < links[j].Page
})
bms := map[string]*pdfcpu.Bookmark{}
footnotes := map[string]*pdfcpu.Bookmark{}
for _, link := range links {
linkID := link.URI[strings.LastIndex(link.URI, "/")+1:]
//if strings.HasPrefix(linkID, "footnotes-def-") { // 导出 PDF 时块引转换脚注使用书签跳转 https://github.com/siyuan-note/siyuan/issues/5761
// footnotes[linkID] = &pdfcpu.Bookmark{
// Title: "Footnote [^" + linkID + "]",
// PageFrom: link.Page,
// AbsPos: link.Rect.UR.Y,
// Level: 7,
// }
// continue
//}
title := sql.GetBlock(linkID).Content
title, _ = url.QueryUnescape(title)
bm := &pdfcpu.Bookmark{
Title: title,
PageFrom: link.Page,
AbsPos: link.Rect.UR.Y,
}
bms[linkID] = bm
}
if 1 > len(bms) && 1 > len(footnotes) {
return
}
tree, _ := loadTreeByBlockID(id)
if nil == tree {
return
}
var headings []*ast.Node
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if entering && ast.NodeHeading == n.Type && !n.ParentIs(ast.NodeBlockquote) {
headings = append(headings, n)
return ast.WalkSkipChildren
}
return ast.WalkContinue
})
var topBms []*pdfcpu.Bookmark
stack := linkedliststack.New()
for _, h := range headings {
L:
for ; ; stack.Pop() {
cur, ok := stack.Peek()
if !ok {
bm := bms[h.ID]
if nil == bm {
break L
}
bm.Level = h.HeadingLevel
stack.Push(bm)
topBms = append(topBms, bm)
break L
}
tip := cur.(*pdfcpu.Bookmark)
if tip.Level < h.HeadingLevel {
bm := bms[h.ID]
bm.Level = h.HeadingLevel
bm.Parent = tip
tip.Children = append(tip.Children, bm)
stack.Push(bm)
break L
}
}
}
//if 4 == Conf.Export.BlockRefMode { // 块引转脚注
// var footnotesBms []*pdfcpu.Bookmark
// for _, bm := range footnotes {
// footnotesBms = append(footnotesBms, bm)
// }
// sort.Slice(footnotesBms, func(i, j int) bool { return footnotesBms[i].PageFrom < footnotesBms[j].PageFrom })
// for _, bm := range footnotesBms {
// topBms = append(topBms, bm)
// }
//}
outFile := inFile + ".tmp"
err = api.AddBookmarksFile(inFile, outFile, topBms, nil)
if nil != err {
logging.LogErrorf("add bookmark failed: %s", err)
return
}
err = os.Rename(outFile, inFile)
return
}
func CopyStdMarkdown(id string) string {
tree, _ := loadTreeByBlockID(id)
tree = exportTree(tree, false, false, false)
luteEngine := NewLute()
luteEngine.SetFootnotes(true)
luteEngine.SetKramdownIAL(false)
if IsSubscriber() {
// 订阅用户使用云端图床服务
luteEngine.RenderOptions.LinkBase = "https://assets.b3logfile.com/siyuan/" + Conf.User.UserId + "/"
}
return treenode.ExportNodeStdMd(tree.Root, luteEngine)
}
func ExportMarkdown(id string) (name, zipPath string) {
block := treenode.GetBlockTree(id)
if nil == block {
logging.LogErrorf("not found block [%s]", id)
return
}
boxID := block.BoxID
box := Conf.Box(boxID)
baseFolderName := path.Base(block.HPath)
if "." == baseFolderName {
baseFolderName = path.Base(block.Path)
}
docPaths := []string{block.Path}
docFiles := box.ListFiles(strings.TrimSuffix(block.Path, ".sy"))
for _, docFile := range docFiles {
docPaths = append(docPaths, docFile.path)
}
zipPath = exportMarkdownZip(boxID, baseFolderName, docPaths)
name = strings.TrimSuffix(filepath.Base(block.Path), ".sy")
return
}
func BatchExportMarkdown(boxID, folderPath string) (zipPath string) {
box := Conf.Box(boxID)
var baseFolderName string
if "/" == folderPath {
baseFolderName = box.Name
} else {
block := treenode.GetBlockTreeRootByHPath(box.ID, folderPath)
if nil == block {
logging.LogErrorf("not found block")
return
}
baseFolderName = path.Base(block.HPath)
}
if "" == baseFolderName {
baseFolderName = "Untitled"
}
docFiles := box.ListFiles(folderPath)
var docPaths []string
for _, docFile := range docFiles {
docPaths = append(docPaths, docFile.path)
}
zipPath = exportMarkdownZip(boxID, baseFolderName, docPaths)
return
}
func exportMarkdownZip(boxID, baseFolderName string, docPaths []string) (zipPath string) {
dir, name := path.Split(baseFolderName)
name = util.FilterFileName(name)
if strings.HasSuffix(name, "..") {
// 文档标题以 `..` 结尾时无法导出 Markdown https://github.com/siyuan-note/siyuan/issues/4698
// 似乎是 os.MkdirAll 的 bug以 .. 结尾的路径无法创建,所以这里加上 _ 结尾
name += "_"
}
baseFolderName = path.Join(dir, name)
box := Conf.Box(boxID)
exportFolder := filepath.Join(util.TempDir, "export", baseFolderName)
if err := os.MkdirAll(exportFolder, 0755); nil != err {
logging.LogErrorf("create export temp folder failed: %s", err)
return
}
luteEngine := util.NewLute()
for _, p := range docPaths {
docIAL := box.docIAL(p)
if nil == docIAL {
continue
}
id := docIAL["id"]
hPath, md := exportMarkdownContent(id)
dir, name = path.Split(hPath)
dir = util.FilterFilePath(dir) // 导出文档时未移除不支持的文件名符号 https://github.com/siyuan-note/siyuan/issues/4590
name = util.FilterFileName(name)
hPath = path.Join(dir, name)
p = hPath + ".md"
writePath := filepath.Join(exportFolder, p)
if gulu.File.IsExist(writePath) {
// 重名文档加 ID
p = hPath + "-" + id + ".md"
writePath = filepath.Join(exportFolder, p)
}
writeFolder := filepath.Dir(writePath)
if err := os.MkdirAll(writeFolder, 0755); nil != err {
logging.LogErrorf("create export temp folder [%s] failed: %s", writeFolder, err)
continue
}
if err := gulu.File.WriteFileSafer(writePath, gulu.Str.ToBytes(md), 0644); nil != err {
logging.LogErrorf("write export markdown file [%s] failed: %s", writePath, err)
continue
}
// 解析导出后的标准 Markdown汇总 assets
tree := parse.Parse("", gulu.Str.ToBytes(md), luteEngine.ParseOptions)
var assets []string
assets = append(assets, assetsLinkDestsInTree(tree)...)
for _, asset := range assets {
asset = string(html.DecodeDestination([]byte(asset)))
if strings.Contains(asset, "?") {
asset = asset[:strings.LastIndex(asset, "?")]
}
srcPath, err := GetAssetAbsPath(asset)
if nil != err {
logging.LogWarnf("get asset [%s] abs path failed: %s", asset, err)
continue
}
destPath := filepath.Join(writeFolder, asset)
if gulu.File.IsDir(srcPath) {
err = gulu.File.Copy(srcPath, destPath)
} else {
err = gulu.File.CopyFile(srcPath, destPath)
}
if nil != err {
logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", srcPath, destPath, err)
continue
}
}
}
zipPath = exportFolder + ".zip"
zip, err := gulu.Zip.Create(zipPath)
if nil != err {
logging.LogErrorf("create export markdown zip [%s] failed: %s", exportFolder, err)
return ""
}
if err = zip.AddDirectory(baseFolderName, exportFolder); nil != err {
logging.LogErrorf("create export markdown zip [%s] failed: %s", exportFolder, err)
return ""
}
if err = zip.Close(); nil != err {
logging.LogErrorf("close export markdown zip failed: %s", err)
}
os.RemoveAll(exportFolder)
zipPath = "/export/" + url.PathEscape(filepath.Base(zipPath))
return
}
func exportBoxSYZip(boxID string) (zipPath string) {
box := Conf.Box(boxID)
if nil == box {
logging.LogErrorf("not found box [%s]", boxID)
return
}
baseFolderName := box.Name
var docPaths []string
docFiles := box.ListFiles("/")
for _, docFile := range docFiles {
docPaths = append(docPaths, docFile.path)
}
zipPath = exportSYZip(boxID, "/", baseFolderName, docPaths)
return
}
func exportSYZip(boxID, rootDirPath, baseFolderName string, docPaths []string) (zipPath string) {
dir, name := path.Split(baseFolderName)
name = util.FilterFileName(name)
if strings.HasSuffix(name, "..") {
// 文档标题以 `..` 结尾时无法导出 Markdown https://github.com/siyuan-note/siyuan/issues/4698
// 似乎是 os.MkdirAll 的 bug以 .. 结尾的路径无法创建,所以这里加上 _ 结尾
name += "_"
}
baseFolderName = path.Join(dir, name)
box := Conf.Box(boxID)
exportFolder := filepath.Join(util.TempDir, "export", baseFolderName)
if err := os.MkdirAll(exportFolder, 0755); nil != err {
logging.LogErrorf("create export temp folder failed: %s", err)
return
}
trees := map[string]*parse.Tree{}
refTrees := map[string]*parse.Tree{}
for _, p := range docPaths {
docIAL := box.docIAL(p)
if nil == docIAL {
continue
}
id := docIAL["id"]
tree, err := loadTreeByBlockID(id)
if nil != err {
continue
}
trees[tree.ID] = tree
}
for _, tree := range trees {
refs := exportRefTrees(tree)
for refTreeID, refTree := range refs {
if nil == trees[refTreeID] {
refTrees[refTreeID] = refTree
}
}
}
// 按文件夹结构复制选择的树
for _, tree := range trees {
readPath := filepath.Join(util.DataDir, tree.Box, tree.Path)
data, readErr := filelock.ReadFile(readPath)
if nil != readErr {
logging.LogErrorf("read file [%s] failed: %s", readPath, readErr)
continue
}
writePath := strings.TrimPrefix(tree.Path, rootDirPath)
writePath = filepath.Join(exportFolder, writePath)
writeFolder := filepath.Dir(writePath)
if mkdirErr := os.MkdirAll(writeFolder, 0755); nil != mkdirErr {
logging.LogErrorf("create export temp folder [%s] failed: %s", writeFolder, mkdirErr)
continue
}
if writeErr := os.WriteFile(writePath, data, 0644); nil != writeErr {
logging.LogErrorf("write export file [%s] failed: %s", writePath, writeErr)
continue
}
}
// 引用树放在导出文件夹根路径下
for treeID, tree := range refTrees {
readPath := filepath.Join(util.DataDir, tree.Box, tree.Path)
data, readErr := filelock.ReadFile(readPath)
if nil != readErr {
logging.LogErrorf("read file [%s] failed: %s", readPath, readErr)
continue
}
writePath := strings.TrimPrefix(tree.Path, rootDirPath)
writePath = filepath.Join(exportFolder, treeID+".sy")
if writeErr := os.WriteFile(writePath, data, 0644); nil != writeErr {
logging.LogErrorf("write export file [%s] failed: %s", writePath, writeErr)
continue
}
}
// 将引用树合并到选择树中,以便后面一次性导出资源文件
for treeID, tree := range refTrees {
trees[treeID] = tree
}
// 导出引用的资源文件
copiedAssets := hashset.New()
for _, tree := range trees {
var assets []string
assets = append(assets, assetsLinkDestsInTree(tree)...)
for _, asset := range assets {
asset = string(html.DecodeDestination([]byte(asset)))
if strings.Contains(asset, "?") {
asset = asset[:strings.LastIndex(asset, "?")]
}
if copiedAssets.Contains(asset) {
continue
}
srcPath, assetErr := GetAssetAbsPath(asset)
if nil != assetErr {
logging.LogWarnf("get asset [%s] abs path failed: %s", asset, assetErr)
continue
}
destPath := filepath.Join(exportFolder, asset)
if gulu.File.IsDir(srcPath) {
assetErr = gulu.File.Copy(srcPath, destPath)
} else {
assetErr = gulu.File.CopyFile(srcPath, destPath)
}
if nil != assetErr {
logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", srcPath, destPath, assetErr)
continue
}
copiedAssets.Add(asset)
}
}
// 导出自定义排序
sortPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json")
fullSortIDs := map[string]int{}
sortIDs := map[string]int{}
var sortData []byte
var sortErr error
if gulu.File.IsExist(sortPath) {
sortData, sortErr = filelock.ReadFile(sortPath)
if nil != sortErr {
logging.LogErrorf("read sort conf failed: %s", sortErr)
}
if sortErr = gulu.JSON.UnmarshalJSON(sortData, &fullSortIDs); nil != sortErr {
logging.LogErrorf("unmarshal sort conf failed: %s", sortErr)
}
if 0 < len(fullSortIDs) {
for _, tree := range trees {
if v, ok := fullSortIDs[tree.ID]; ok {
sortIDs[tree.ID] = v
}
}
}
if 0 < len(sortIDs) {
sortData, sortErr = gulu.JSON.MarshalJSON(sortIDs)
if nil != sortErr {
logging.LogErrorf("marshal sort conf failed: %s", sortErr)
}
if 0 < len(sortData) {
confDir := filepath.Join(exportFolder, ".siyuan")
if mkdirErr := os.MkdirAll(confDir, 0755); nil != mkdirErr {
logging.LogErrorf("create export conf folder [%s] failed: %s", confDir, mkdirErr)
} else {
sortPath = filepath.Join(confDir, "sort.json")
if writeErr := os.WriteFile(sortPath, sortData, 0644); nil != writeErr {
logging.LogErrorf("write sort conf failed: %s", writeErr)
}
}
}
}
}
zipPath = exportFolder + ".sy.zip"
zip, err := gulu.Zip.Create(zipPath)
if nil != err {
logging.LogErrorf("create export markdown zip [%s] failed: %s", exportFolder, err)
return ""
}
if err = zip.AddDirectory(baseFolderName, exportFolder); nil != err {
logging.LogErrorf("create export markdown zip [%s] failed: %s", exportFolder, err)
return ""
}
if err = zip.Close(); nil != err {
logging.LogErrorf("close export markdown zip failed: %s", err)
}
os.RemoveAll(exportFolder)
zipPath = "/export/" + url.PathEscape(filepath.Base(zipPath))
return
}
func ExportMarkdownContent(id string) (hPath, exportedMd string) {
return exportMarkdownContent(id)
}
func exportMarkdownContent(id string) (hPath, exportedMd string) {
tree, _ := loadTreeByBlockID(id)
hPath = tree.HPath
tree = exportTree(tree, false, true, false)
luteEngine := NewLute()
luteEngine.SetFootnotes(true)
luteEngine.SetKramdownIAL(false)
renderer := render.NewProtyleExportMdRenderer(tree, luteEngine.RenderOptions)
exportedMd = gulu.Str.FromBytes(renderer.Render())
return
}
func processKaTexMacros(n *ast.Node) {
if ast.NodeInlineMathContent != n.Type && ast.NodeMathBlockContent != n.Type && ast.NodeTextMark != n.Type {
return
}
if ast.NodeTextMark == n.Type && !n.IsTextMarkType("inline-math") {
return
}
var mathContent string
if ast.NodeTextMark == n.Type {
mathContent = n.TextMarkInlineMathContent
} else {
mathContent = string(n.Tokens)
}
mathContent = strings.TrimSpace(mathContent)
if "" == mathContent {
return
}
macros := map[string]string{}
if err := gulu.JSON.UnmarshalJSON([]byte(Conf.Editor.KaTexMacros), &macros); nil != err {
logging.LogWarnf("parse katex macros failed: %s", err)
return
}
var keys []string
for k := range macros {
keys = append(keys, k)
}
useMacro := false
for k := range macros {
if strings.Contains(mathContent, k) {
useMacro = true
break
}
}
if !useMacro {
return
}
sort.Slice(keys, func(i, j int) bool { return len(keys[i]) > len(keys[j]) })
mathContent = escapeKaTexSupportedFunctions(mathContent)
usedMacros := extractUsedMacros(mathContent, &keys)
for _, usedMacro := range usedMacros {
expanded := resolveKaTexMacro(usedMacro, &macros, &keys)
expanded = unescapeKaTexSupportedFunctions(expanded)
mathContent = strings.ReplaceAll(mathContent, usedMacro, expanded)
}
mathContent = unescapeKaTexSupportedFunctions(mathContent)
if ast.NodeTextMark == n.Type {
n.TextMarkInlineMathContent = mathContent
} else {
n.Tokens = []byte(mathContent)
}
}
func exportTree(tree *parse.Tree, wysiwyg, expandKaTexMacros, keepFold bool) (ret *parse.Tree) {
luteEngine := NewLute()
ret = tree
id := tree.Root.ID
var unlinks []*ast.Node
// 解析查询嵌入节点
ast.Walk(ret.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || ast.NodeBlockQueryEmbed != n.Type {
return ast.WalkContinue
}
var defMd string
stmt := n.ChildByType(ast.NodeBlockQueryEmbedScript).TokensStr()
stmt = html.UnescapeString(stmt)
blocks := searchEmbedBlock(stmt, nil, 0)
if 1 > len(blocks) {
return ast.WalkContinue
}
defMdBuf := bytes.Buffer{}
for _, def := range blocks {
defMdBuf.WriteString(renderBlockMarkdownR(def.ID))
defMdBuf.WriteString("\n\n")
}
defMd = defMdBuf.String()
buf := &bytes.Buffer{}
lines := strings.Split(defMd, "\n")
for i, line := range lines {
if 0 == Conf.Export.BlockEmbedMode { // 原始文本
buf.WriteString(line)
} else { // Blockquote
buf.WriteString("> " + line)
}
if i < len(lines)-1 {
buf.WriteString("\n")
}
}
buf.WriteString("\n\n")
refTree := parse.Parse("", buf.Bytes(), luteEngine.ParseOptions)
var children []*ast.Node
for c := refTree.Root.FirstChild; nil != c; c = c.Next {
children = append(children, c)
}
for _, c := range children {
if ast.NodeDocument == c.Type {
continue
}
n.InsertBefore(c)
}
unlinks = append(unlinks, n)
return ast.WalkSkipChildren
})
for _, n := range unlinks {
n.Unlink()
}
unlinks = nil
// 收集引用转脚注
var refFootnotes []*refAsFootnotes
if 4 == Conf.Export.BlockRefMode { // 块引转脚注
treeCache := map[string]*parse.Tree{}
treeCache[id] = ret
depth := 0
collectFootnotesDefs(ret.ID, &refFootnotes, &treeCache, &depth)
}
ast.Walk(ret.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
switch n.Type {
case ast.NodeTagOpenMarker: // 配置标签开始标记符
if !wysiwyg {
n.Type = ast.NodeText
n.Tokens = []byte(Conf.Export.TagOpenMarker)
return ast.WalkContinue
}
case ast.NodeTagCloseMarker: // 配置标记结束标记符
if !wysiwyg {
n.Type = ast.NodeText
n.Tokens = []byte(Conf.Export.TagCloseMarker)
return ast.WalkContinue
}
case ast.NodeSuperBlockOpenMarker, ast.NodeSuperBlockLayoutMarker, ast.NodeSuperBlockCloseMarker:
if !wysiwyg {
unlinks = append(unlinks, n)
return ast.WalkContinue
}
case ast.NodeHeading:
n.HeadingNormalizedID = n.IALAttr("id")
n.ID = n.HeadingNormalizedID
case ast.NodeInlineMathContent, ast.NodeMathBlockContent:
n.Tokens = bytes.TrimSpace(n.Tokens) // 导出 Markdown 时去除公式内容中的首尾空格 https://github.com/siyuan-note/siyuan/issues/4666
return ast.WalkContinue
case ast.NodeTextMark:
if n.IsTextMarkType("inline-math") {
n.TextMarkInlineMathContent = strings.TrimSpace(n.TextMarkInlineMathContent)
return ast.WalkContinue
} else if n.IsTextMarkType("file-annotation-ref") {
refID := n.TextMarkFileAnnotationRefID
status := processFileAnnotationRef(refID, n)
unlinks = append(unlinks, n)
return status
} else if n.IsTextMarkType("tag") {
if !wysiwyg {
n.Type = ast.NodeText
n.Tokens = []byte(Conf.Export.TagOpenMarker + n.TextMarkTextContent + Conf.Export.TagCloseMarker)
return ast.WalkContinue
}
}
case ast.NodeFileAnnotationRef:
refIDNode := n.ChildByType(ast.NodeFileAnnotationRefID)
if nil == refIDNode {
return ast.WalkSkipChildren
}
refID := refIDNode.TokensStr()
status := processFileAnnotationRef(refID, n)
unlinks = append(unlinks, n)
return status
}
if !treenode.IsBlockRef(n) {
return ast.WalkContinue
}
// 处理引用节点
defID, linkText, _ := treenode.GetBlockRef(n)
if "" == linkText {
linkText = sql.GetRefText(defID)
}
if Conf.Editor.BlockRefDynamicAnchorTextMaxLen < utf8.RuneCountInString(linkText) {
linkText = gulu.Str.SubStr(linkText, Conf.Editor.BlockRefDynamicAnchorTextMaxLen) + "..."
}
linkText = Conf.Export.BlockRefTextLeft + linkText + Conf.Export.BlockRefTextRight
defTree, _ := loadTreeByBlockID(defID)
if nil == defTree {
return ast.WalkContinue
}
switch Conf.Export.BlockRefMode {
case 2: // 锚文本块链
var blockRefLink *ast.Node
blockRefLink = &ast.Node{Type: ast.NodeLink}
blockRefLink.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
blockRefLink.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte(linkText)})
blockRefLink.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
blockRefLink.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
blockRefLink.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte("siyuan://blocks/" + defID)})
blockRefLink.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
n.InsertBefore(blockRefLink)
case 3: // 仅锚文本
n.InsertBefore(&ast.Node{Type: ast.NodeText, Tokens: []byte(linkText)})
case 4: // 脚注
refFoot := getRefAsFootnotes(defID, &refFootnotes)
n.InsertBefore(&ast.Node{Type: ast.NodeText, Tokens: []byte(linkText)})
n.InsertBefore(&ast.Node{Type: ast.NodeFootnotesRef, Tokens: []byte("^" + refFoot.refNum), FootnotesRefId: refFoot.refNum, FootnotesRefLabel: []byte("^" + refFoot.refNum)})
}
unlinks = append(unlinks, n)
if nil != n.Next && ast.NodeKramdownSpanIAL == n.Next.Type {
// 引用加排版标记(比如颜色)重叠时丢弃后面的排版属性节点
unlinks = append(unlinks, n.Next)
}
return ast.WalkSkipChildren
})
for _, n := range unlinks {
n.Unlink()
}
if 4 == Conf.Export.BlockRefMode { // 块引转脚注
if footnotesDefBlock := resolveFootnotesDefs(&refFootnotes, ret.Root.ID); nil != footnotesDefBlock {
ret.Root.AppendChild(footnotesDefBlock)
}
}
if Conf.Export.AddTitle {
if root, _ := getBlock(id); nil != root {
title := &ast.Node{Type: ast.NodeHeading, HeadingLevel: 1}
content := html.UnescapeString(root.Content)
title.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(content)})
ret.Root.PrependChild(title)
}
}
// 导出时支持导出题头图 https://github.com/siyuan-note/siyuan/issues/4372
titleImgPath := treenode.GetDocTitleImgPath(ret.Root)
if "" != titleImgPath {
p := &ast.Node{Type: ast.NodeParagraph}
titleImg := &ast.Node{Type: ast.NodeImage}
titleImg.AppendChild(&ast.Node{Type: ast.NodeBang})
titleImg.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
titleImg.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte("image")})
titleImg.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
titleImg.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
titleImg.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(titleImgPath)})
titleImg.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
p.AppendChild(titleImg)
ret.Root.PrependChild(p)
}
unlinks = nil
var emptyParagraphs []*ast.Node
ast.Walk(ret.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
// 支持按照现有折叠状态导出 PDF https://github.com/siyuan-note/siyuan/issues/5941
if !keepFold {
// 块折叠以后导出 HTML/PDF 固定展开 https://github.com/siyuan-note/siyuan/issues/4064
n.RemoveIALAttr("fold")
n.RemoveIALAttr("heading-fold")
} else {
if "1" == n.IALAttr("heading-fold") {
unlinks = append(unlinks, n)
return ast.WalkContinue
}
}
if ast.NodeParagraph == n.Type {
if nil == n.FirstChild {
// 空的段落块需要补全文本展位,否则后续格式化后再解析树会语义不一致 https://github.com/siyuan-note/siyuan/issues/5806
emptyParagraphs = append(emptyParagraphs, n)
}
}
if expandKaTexMacros && (ast.NodeInlineMathContent == n.Type || ast.NodeMathBlockContent == n.Type || (ast.NodeTextMark == n.Type && n.IsTextMarkType("inline-math"))) {
processKaTexMacros(n)
}
if ast.NodeWidget == n.Type {
// 挂件块导出 https://github.com/siyuan-note/siyuan/issues/3834
exportMdVal := n.IALAttr("data-export-md")
exportMdVal = html.UnescapeString(exportMdVal) // 导出 `data-export-md` 时未解析代码块与行内代码内的转义字符 https://github.com/siyuan-note/siyuan/issues/4180
if "" != exportMdVal {
exportMdTree := parse.Parse("", []byte(exportMdVal), luteEngine.ParseOptions)
var insertNodes []*ast.Node
for c := exportMdTree.Root.FirstChild; nil != c; c = c.Next {
if ast.NodeKramdownBlockIAL != c.Type {
insertNodes = append(insertNodes, c)
}
}
for _, insertNode := range insertNodes {
n.InsertBefore(insertNode)
}
unlinks = append(unlinks, n)
}
return ast.WalkContinue
}
if ast.NodeText != n.Type {
return ast.WalkContinue
}
// Shift+Enter 换行在导出为 Markdown 时使用硬换行 https://github.com/siyuan-note/siyuan/issues/3458
n.Tokens = bytes.ReplaceAll(n.Tokens, []byte("\n"), []byte(" \n"))
return ast.WalkContinue
})
for _, n := range unlinks {
n.Unlink()
}
for _, emptyParagraph := range emptyParagraphs {
emptyParagraph.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(editor.Zwj)})
}
return ret
}
func resolveFootnotesDefs(refFootnotes *[]*refAsFootnotes, rootID string) (footnotesDefBlock *ast.Node) {
if 1 > len(*refFootnotes) {
return nil
}
footnotesDefBlock = &ast.Node{Type: ast.NodeFootnotesDefBlock}
var rendered []string
for _, foot := range *refFootnotes {
t, err := loadTreeByBlockID(foot.defID)
if nil != err {
continue
}
defNode := treenode.GetNodeInTree(t, foot.defID)
docID := strings.TrimSuffix(path.Base(defNode.Path), ".sy")
var nodes []*ast.Node
if ast.NodeHeading == defNode.Type {
nodes = append(nodes, defNode)
if rootID != docID {
// 同文档块引转脚注缩略定义考虑容器块和标题块 https://github.com/siyuan-note/siyuan/issues/5917
children := treenode.HeadingChildren(defNode)
nodes = append(nodes, children...)
}
} else if ast.NodeDocument == defNode.Type {
docTitle := &ast.Node{ID: defNode.ID, Type: ast.NodeHeading, HeadingLevel: 1}
docTitle.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(defNode.IALAttr("title"))})
nodes = append(nodes, docTitle)
for c := defNode.FirstChild; nil != c; c = c.Next {
nodes = append(nodes, c)
}
} else {
nodes = append(nodes, defNode)
}
var newNodes []*ast.Node
for _, node := range nodes {
var unlinks []*ast.Node
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if treenode.IsBlockRef(n) {
defID, _, _ := treenode.GetBlockRef(n)
if f := getRefAsFootnotes(defID, refFootnotes); nil != f {
n.InsertBefore(&ast.Node{Type: ast.NodeText, Tokens: []byte(Conf.Export.BlockRefTextLeft + f.refAnchorText + Conf.Export.BlockRefTextRight)})
n.InsertBefore(&ast.Node{Type: ast.NodeFootnotesRef, Tokens: []byte("^" + f.refNum), FootnotesRefId: f.refNum, FootnotesRefLabel: []byte("^" + f.refNum)})
unlinks = append(unlinks, n)
}
return ast.WalkSkipChildren
} else if ast.NodeBlockQueryEmbed == n.Type {
stmt := n.ChildByType(ast.NodeBlockQueryEmbedScript).TokensStr()
stmt = html.UnescapeString(stmt)
sqlBlocks := sql.SelectBlocksRawStmt(stmt, Conf.Search.Limit)
for _, b := range sqlBlocks {
subNodes := renderBlockMarkdownR0(b.ID, &rendered)
for _, subNode := range subNodes {
if ast.NodeListItem == subNode.Type {
parentList := &ast.Node{Type: ast.NodeList, ListData: &ast.ListData{Typ: subNode.ListData.Typ}}
parentList.AppendChild(subNode)
newNodes = append(newNodes, parentList)
} else {
newNodes = append(newNodes, subNode)
}
}
}
unlinks = append(unlinks, n)
return ast.WalkSkipChildren
}
return ast.WalkContinue
})
for _, n := range unlinks {
n.Unlink()
}
if ast.NodeBlockQueryEmbed != node.Type {
if ast.NodeListItem == node.Type {
parentList := &ast.Node{Type: ast.NodeList, ListData: &ast.ListData{Typ: node.ListData.Typ}}
parentList.AppendChild(node)
newNodes = append(newNodes, parentList)
} else {
newNodes = append(newNodes, node)
}
}
}
footnotesDef := &ast.Node{Type: ast.NodeFootnotesDef, Tokens: []byte("^" + foot.refNum), FootnotesRefId: foot.refNum, FootnotesRefLabel: []byte("^" + foot.refNum)}
for _, node := range newNodes {
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeParagraph != n.Type {
return ast.WalkContinue
}
docID := strings.TrimSuffix(path.Base(n.Path), ".sy")
if rootID == docID {
// 同文档块引转脚注缩略定义 https://github.com/siyuan-note/siyuan/issues/3299
if text := sql.GetRefText(n.ID); 64 < utf8.RuneCountInString(text) {
var unlinkChildren []*ast.Node
for c := n.FirstChild; nil != c; c = c.Next {
unlinkChildren = append(unlinkChildren, c)
}
for _, c := range unlinkChildren {
c.Unlink()
}
text = gulu.Str.SubStr(text, 64) + "..."
n.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(text)})
return ast.WalkSkipChildren
}
}
return ast.WalkContinue
})
footnotesDef.AppendChild(node)
}
footnotesDefBlock.AppendChild(footnotesDef)
}
return
}
func collectFootnotesDefs(id string, refFootnotes *[]*refAsFootnotes, treeCache *map[string]*parse.Tree, depth *int) {
*depth++
if 4096 < *depth {
return
}
b := treenode.GetBlockTree(id)
if nil == b {
return
}
t := (*treeCache)[b.RootID]
if nil == t {
var err error
if t, err = loadTreeByBlockID(b.ID); nil != err {
return
}
(*treeCache)[t.ID] = t
}
node := treenode.GetNodeInTree(t, b.ID)
if nil == node {
logging.LogErrorf("not found node [%s] in tree [%s]", b.ID, t.Root.ID)
return
}
collectFootnotesDefs0(node, refFootnotes, treeCache, depth)
if ast.NodeHeading == node.Type {
children := treenode.HeadingChildren(node)
for _, c := range children {
collectFootnotesDefs0(c, refFootnotes, treeCache, depth)
}
}
return
}
func collectFootnotesDefs0(node *ast.Node, refFootnotes *[]*refAsFootnotes, treeCache *map[string]*parse.Tree, depth *int) {
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if treenode.IsBlockRef(n) {
defID, _, _ := treenode.GetBlockRef(n)
if nil == getRefAsFootnotes(defID, refFootnotes) {
anchorText := sql.GetRefText(defID)
if Conf.Editor.BlockRefDynamicAnchorTextMaxLen < utf8.RuneCountInString(anchorText) {
anchorText = gulu.Str.SubStr(anchorText, Conf.Editor.BlockRefDynamicAnchorTextMaxLen) + "..."
}
*refFootnotes = append(*refFootnotes, &refAsFootnotes{
defID: defID,
refNum: strconv.Itoa(len(*refFootnotes) + 1),
refAnchorText: anchorText,
})
collectFootnotesDefs(defID, refFootnotes, treeCache, depth)
}
return ast.WalkSkipChildren
}
return ast.WalkContinue
})
}
func getRefAsFootnotes(defID string, slice *[]*refAsFootnotes) *refAsFootnotes {
for _, e := range *slice {
if e.defID == defID {
return e
}
}
return nil
}
type refAsFootnotes struct {
defID string
refNum string
refAnchorText string
}
func exportRefTrees(tree *parse.Tree) (ret map[string]*parse.Tree) {
ret = map[string]*parse.Tree{}
exportRefTrees0(tree, &ret)
return
}
func exportRefTrees0(tree *parse.Tree, retTrees *map[string]*parse.Tree) {
if nil != (*retTrees)[tree.ID] {
return
}
(*retTrees)[tree.ID] = tree
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if treenode.IsBlockRef(n) {
defID, _, _ := treenode.GetBlockRef(n)
if "" == defID {
return ast.WalkContinue
}
defBlock := treenode.GetBlockTree(defID)
if nil == defBlock {
return ast.WalkSkipChildren
}
defTree, err := loadTreeByBlockID(defBlock.RootID)
if nil != err {
return ast.WalkSkipChildren
}
exportRefTrees0(defTree, retTrees)
}
return ast.WalkContinue
})
}
func processFileAnnotationRef(refID string, n *ast.Node) ast.WalkStatus {
p := refID[:strings.LastIndex(refID, "/")]
absPath, err := GetAssetAbsPath(p)
if nil != err {
logging.LogWarnf("get assets abs path by rel path [%s] failed: %s", p, err)
return ast.WalkSkipChildren
}
sya := absPath + ".sya"
syaData, err := os.ReadFile(sya)
if nil != err {
logging.LogErrorf("read file [%s] failed: %s", sya, err)
return ast.WalkSkipChildren
}
syaJSON := map[string]interface{}{}
if err = gulu.JSON.UnmarshalJSON(syaData, &syaJSON); nil != err {
logging.LogErrorf("unmarshal file [%s] failed: %s", sya, err)
return ast.WalkSkipChildren
}
annotationID := refID[strings.LastIndex(refID, "/")+1:]
annotationData := syaJSON[annotationID]
if nil == annotationData {
logging.LogErrorf("not found annotation [%s] in .sya", annotationID)
return ast.WalkSkipChildren
}
pages := annotationData.(map[string]interface{})["pages"].([]interface{})
page := int(pages[0].(map[string]interface{})["index"].(float64)) + 1
pageStr := strconv.Itoa(page)
var refText string
if ast.NodeTextMark == n.Type {
refText = n.TextMarkTextContent
} else {
refTextNode := n.ChildByType(ast.NodeFileAnnotationRefText)
if nil == refTextNode {
return ast.WalkSkipChildren
}
refText = refTextNode.TokensStr()
}
ext := filepath.Ext(p)
file := p[7:len(p)-23-len(ext)] + ext
fileAnnotationRefLink := &ast.Node{Type: ast.NodeLink}
fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
if 0 == Conf.Export.FileAnnotationRefMode {
fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte(file + " - p" + pageStr + " - " + refText)})
} else {
fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte(refText)})
}
fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(p + "?p=" + pageStr)})
fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
n.InsertBefore(fileAnnotationRefLink)
return ast.WalkSkipChildren
}