A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
1// SiYuan - Refactor your thinking
2// Copyright (c) 2020-present, b3log.org
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17package model
18
19import (
20 "bytes"
21 "crypto/sha1"
22 "encoding/csv"
23 "errors"
24 "fmt"
25 "net/http"
26 "net/url"
27 "os"
28 "os/exec"
29 "path"
30 "path/filepath"
31 "sort"
32 "strconv"
33 "strings"
34 "time"
35 "unicode/utf8"
36
37 "github.com/88250/go-humanize"
38 "github.com/88250/gulu"
39 "github.com/88250/lute/ast"
40 "github.com/88250/lute/editor"
41 "github.com/88250/lute/html"
42 "github.com/88250/lute/lex"
43 "github.com/88250/lute/parse"
44 "github.com/88250/lute/render"
45 "github.com/emirpasic/gods/sets/hashset"
46 "github.com/emirpasic/gods/stacks/linkedliststack"
47 "github.com/imroc/req/v3"
48 "github.com/pdfcpu/pdfcpu/pkg/api"
49 "github.com/pdfcpu/pdfcpu/pkg/font"
50 "github.com/pdfcpu/pdfcpu/pkg/pdfcpu"
51 "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
52 "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types"
53 "github.com/siyuan-note/filelock"
54 "github.com/siyuan-note/httpclient"
55 "github.com/siyuan-note/logging"
56 "github.com/siyuan-note/riff"
57 "github.com/siyuan-note/siyuan/kernel/av"
58 "github.com/siyuan-note/siyuan/kernel/filesys"
59 "github.com/siyuan-note/siyuan/kernel/sql"
60 "github.com/siyuan-note/siyuan/kernel/treenode"
61 "github.com/siyuan-note/siyuan/kernel/util"
62)
63
64func ExportAv2CSV(avID, blockID string) (zipPath string, err error) {
65 // Database block supports export as CSV https://github.com/siyuan-note/siyuan/issues/10072
66
67 attrView, err := av.ParseAttributeView(avID)
68 if err != nil {
69 return
70 }
71
72 node, _, err := getNodeByBlockID(nil, blockID)
73 if nil == node {
74 return
75 }
76 viewID := node.IALAttr(av.NodeAttrView)
77 view, err := attrView.GetCurrentView(viewID)
78 if err != nil {
79 return
80 }
81
82 name := util.FilterFileName(getAttrViewName(attrView))
83 table := getAttrViewTable(attrView, view, "")
84
85 // 遵循视图过滤和排序规则 Use filtering and sorting of current view settings when exporting database blocks https://github.com/siyuan-note/siyuan/issues/10474
86 cachedAttrViews := map[string]*av.AttributeView{}
87 rollupFurtherCollections := sql.GetFurtherCollections(attrView, cachedAttrViews)
88 av.Filter(table, attrView, rollupFurtherCollections, cachedAttrViews)
89 av.Sort(table, attrView)
90
91 exportFolder := filepath.Join(util.TempDir, "export", "csv", name)
92 if err = os.MkdirAll(exportFolder, 0755); err != nil {
93 logging.LogErrorf("mkdir [%s] failed: %s", exportFolder, err)
94 return
95 }
96 csvPath := filepath.Join(exportFolder, name+".csv")
97
98 f, err := os.OpenFile(csvPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
99 if err != nil {
100 logging.LogErrorf("open [%s] failed: %s", csvPath, err)
101 return
102 }
103
104 if _, err = f.WriteString("\xEF\xBB\xBF"); err != nil { // 写入 UTF-8 BOM,避免使用 Microsoft Excel 打开乱码
105 logging.LogErrorf("write UTF-8 BOM to [%s] failed: %s", csvPath, err)
106 f.Close()
107 return
108 }
109
110 writer := csv.NewWriter(f)
111 var header []string
112 for _, col := range table.Columns {
113 header = append(header, col.Name)
114 }
115 if err = writer.Write(header); err != nil {
116 logging.LogErrorf("write csv header [%s] failed: %s", header, err)
117 f.Close()
118 return
119 }
120
121 rowNum := 1
122 for _, row := range table.Rows {
123 var rowVal []string
124 for _, cell := range row.Cells {
125 var val string
126 if nil != cell.Value {
127 if av.KeyTypeDate == cell.Value.Type {
128 if nil != cell.Value.Date {
129 cell.Value.Date = av.NewFormattedValueDate(cell.Value.Date.Content, cell.Value.Date.Content2, av.DateFormatNone, cell.Value.Date.IsNotTime, cell.Value.Date.HasEndDate)
130 }
131 } else if av.KeyTypeCreated == cell.Value.Type {
132 if nil != cell.Value.Created {
133 cell.Value.Created = av.NewFormattedValueCreated(cell.Value.Created.Content, 0, av.CreatedFormatNone)
134 }
135 } else if av.KeyTypeUpdated == cell.Value.Type {
136 if nil != cell.Value.Updated {
137 cell.Value.Updated = av.NewFormattedValueUpdated(cell.Value.Updated.Content, 0, av.UpdatedFormatNone)
138 }
139 } else if av.KeyTypeMAsset == cell.Value.Type {
140 if nil != cell.Value.MAsset {
141 buf := &bytes.Buffer{}
142 for _, a := range cell.Value.MAsset {
143 if av.AssetTypeImage == a.Type {
144 buf.WriteString("
147 buf.WriteString(a.Content)
148 buf.WriteString(") ")
149 } else if av.AssetTypeFile == a.Type {
150 buf.WriteString("[")
151 buf.WriteString(a.Name)
152 buf.WriteString("](")
153 buf.WriteString(a.Content)
154 buf.WriteString(") ")
155 } else {
156 buf.WriteString(a.Content)
157 buf.WriteString(" ")
158 }
159 }
160 val = strings.TrimSpace(buf.String())
161 }
162 } else if av.KeyTypeLineNumber == cell.Value.Type {
163 val = strconv.Itoa(rowNum)
164 }
165
166 if "" == val {
167 val = cell.Value.String(true)
168 }
169 }
170
171 rowVal = append(rowVal, val)
172 }
173 if err = writer.Write(rowVal); err != nil {
174 logging.LogErrorf("write csv row [%s] failed: %s", rowVal, err)
175 f.Close()
176 return
177 }
178 rowNum++
179 }
180 writer.Flush()
181
182 zipPath = exportFolder + ".db.zip"
183 zip, err := gulu.Zip.Create(zipPath)
184 if err != nil {
185 logging.LogErrorf("create export .db.zip [%s] failed: %s", exportFolder, err)
186 f.Close()
187 return
188 }
189
190 if err = zip.AddDirectory("", exportFolder); err != nil {
191 logging.LogErrorf("create export .db.zip [%s] failed: %s", exportFolder, err)
192 f.Close()
193 return
194 }
195
196 if err = zip.Close(); err != nil {
197 logging.LogErrorf("close export .db.zip failed: %s", err)
198 f.Close()
199 return
200 }
201
202 f.Close()
203 removeErr := os.RemoveAll(exportFolder)
204 if nil != removeErr {
205 logging.LogErrorf("remove export folder [%s] failed: %s", exportFolder, removeErr)
206 }
207 zipPath = "/export/csv/" + url.PathEscape(filepath.Base(zipPath))
208 return
209}
210
211func Export2Liandi(id string) (err error) {
212 tree, err := LoadTreeByBlockID(id)
213 if err != nil {
214 logging.LogErrorf("load tree by block id [%s] failed: %s", id, err)
215 return
216 }
217
218 if IsUserGuide(tree.Box) {
219 // Doc in the user guide no longer supports one-click sending to the community https://github.com/siyuan-note/siyuan/issues/8388
220 return errors.New(Conf.Language(204))
221 }
222
223 assets := getAssetsLinkDests(tree.Root)
224 embedAssets := getQueryEmbedNodesAssetsLinkDests(tree.Root)
225 assets = append(assets, embedAssets...)
226 assets = gulu.Str.RemoveDuplicatedElem(assets)
227 _, err = uploadAssets2Cloud(assets, bizTypeExport2Liandi)
228 if err != nil {
229 return
230 }
231
232 msgId := util.PushMsg(Conf.Language(182), 15000)
233 defer util.PushClearMsg(msgId)
234
235 // 判断帖子是否已经存在,存在则使用更新接口
236 const liandiArticleIdAttrName = "custom-liandi-articleId"
237 foundArticle := false
238 articleId := tree.Root.IALAttr(liandiArticleIdAttrName)
239 if "" != articleId {
240 result := gulu.Ret.NewResult()
241 request := httpclient.NewCloudRequest30s()
242 resp, getErr := request.
243 SetSuccessResult(result).
244 SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}).
245 Get(util.GetCloudAccountServer() + "/api/v2/article/update/" + articleId)
246 if nil != getErr {
247 logging.LogErrorf("get liandi article info failed: %s", getErr)
248 return getErr
249 }
250
251 switch resp.StatusCode {
252 case 200:
253 if 0 == result.Code {
254 foundArticle = true
255 } else if 1 == result.Code {
256 foundArticle = false
257 }
258 case 404:
259 foundArticle = false
260 default:
261 err = errors.New(fmt.Sprintf("get liandi article info failed [sc=%d]", resp.StatusCode))
262 return
263 }
264 }
265
266 apiURL := util.GetCloudAccountServer() + "/api/v2/article"
267 if foundArticle {
268 apiURL += "/" + articleId
269 }
270
271 title := path.Base(tree.HPath)
272 tags := tree.Root.IALAttr("tags")
273 content := exportMarkdownContent0(id, tree, util.GetCloudForumAssetsServer()+time.Now().Format("2006/01")+"/siyuan/"+Conf.GetUser().UserId+"/",
274 true, false, false,
275 ".md", 3, 1, 1,
276 "#", "#",
277 "", "",
278 false, false, nil, true, false, map[string]*parse.Tree{})
279 result := gulu.Ret.NewResult()
280 request := httpclient.NewCloudRequest30s()
281 request = request.
282 SetSuccessResult(result).
283 SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}).
284 SetBody(map[string]interface{}{
285 "articleTitle": title,
286 "articleTags": tags,
287 "articleContent": content})
288 var resp *req.Response
289 var sendErr error
290 if foundArticle {
291 resp, sendErr = request.Put(apiURL)
292 } else {
293 resp, sendErr = request.Post(apiURL)
294 }
295 if nil != sendErr {
296 logging.LogErrorf("send article to liandi failed: %s", err)
297 return err
298 }
299 if 200 != resp.StatusCode {
300 msg := fmt.Sprintf("send article to liandi failed [sc=%d]", resp.StatusCode)
301 logging.LogErrorf(msg)
302 return errors.New(msg)
303 }
304
305 if 0 != result.Code {
306 msg := fmt.Sprintf("send article to liandi failed [code=%d, msg=%s]", result.Code, result.Msg)
307 logging.LogErrorf(msg)
308 util.PushClearMsg(msgId)
309 return errors.New(result.Msg)
310 }
311
312 if !foundArticle {
313 articleId = result.Data.(string)
314 tree, _ = LoadTreeByBlockID(id) // 这里必须重新加载,因为前面导出时已经修改了树结构
315 tree.Root.SetIALAttr(liandiArticleIdAttrName, articleId)
316 if err = writeTreeUpsertQueue(tree); err != nil {
317 return
318 }
319 }
320
321 util.PushMsg(fmt.Sprintf(Conf.Language(181), util.GetCloudAccountServer()+"/article/"+articleId), 7000)
322 return
323}
324
325func ExportSystemLog() (zipPath string) {
326 exportFolder := filepath.Join(util.TempDir, "export", "system-log")
327 os.RemoveAll(exportFolder)
328 if err := os.MkdirAll(exportFolder, 0755); err != nil {
329 logging.LogErrorf("create export temp folder failed: %s", err)
330 return
331 }
332
333 appLog := filepath.Join(util.HomeDir, ".config", "siyuan", "app.log")
334 if gulu.File.IsExist(appLog) {
335 to := filepath.Join(exportFolder, "app.log")
336 if err := filelock.Copy(appLog, to); err != nil {
337 logging.LogErrorf("copy app log from [%s] to [%s] failed: %s", err, appLog, to)
338 }
339 }
340
341 kernelLog := filepath.Join(util.HomeDir, ".config", "siyuan", "kernel.log")
342 if gulu.File.IsExist(kernelLog) {
343 to := filepath.Join(exportFolder, "kernel.log")
344 if err := filelock.Copy(kernelLog, to); err != nil {
345 logging.LogErrorf("copy kernel log from [%s] to [%s] failed: %s", err, kernelLog, to)
346 }
347 }
348
349 siyuanLog := filepath.Join(util.TempDir, "siyuan.log")
350 if gulu.File.IsExist(siyuanLog) {
351 to := filepath.Join(exportFolder, "siyuan.log")
352 if err := filelock.Copy(siyuanLog, to); err != nil {
353 logging.LogErrorf("copy kernel log from [%s] to [%s] failed: %s", err, siyuanLog, to)
354 }
355 }
356
357 mobileLog := filepath.Join(util.TempDir, "mobile.log")
358 if gulu.File.IsExist(mobileLog) {
359 to := filepath.Join(exportFolder, "mobile.log")
360 if err := filelock.Copy(mobileLog, to); err != nil {
361 logging.LogErrorf("copy mobile log from [%s] to [%s] failed: %s", err, mobileLog, to)
362 }
363 }
364
365 zipPath = exportFolder + ".zip"
366 zip, err := gulu.Zip.Create(zipPath)
367 if err != nil {
368 logging.LogErrorf("create export log zip [%s] failed: %s", exportFolder, err)
369 return ""
370 }
371
372 if err = zip.AddDirectory("log", exportFolder); err != nil {
373 logging.LogErrorf("create export log zip [%s] failed: %s", exportFolder, err)
374 return ""
375 }
376
377 if err = zip.Close(); err != nil {
378 logging.LogErrorf("close export log zip failed: %s", err)
379 }
380
381 os.RemoveAll(exportFolder)
382 zipPath = "/export/" + url.PathEscape(filepath.Base(zipPath))
383 return
384}
385
386func ExportNotebookSY(id string) (zipPath string) {
387 zipPath = exportBoxSYZip(id)
388 return
389}
390
391func ExportSY(id string) (name, zipPath string) {
392 block := treenode.GetBlockTree(id)
393 if nil == block {
394 logging.LogErrorf("not found block [%s]", id)
395 return
396 }
397
398 boxID := block.BoxID
399 box := Conf.Box(boxID)
400 baseFolderName := path.Base(block.HPath)
401 if "." == baseFolderName {
402 baseFolderName = path.Base(block.Path)
403 }
404 rootPath := block.Path
405 docPaths := []string{rootPath}
406 docFiles := box.ListFiles(strings.TrimSuffix(block.Path, ".sy"))
407 for _, docFile := range docFiles {
408 docPaths = append(docPaths, docFile.path)
409 }
410 zipPath = exportSYZip(boxID, path.Dir(rootPath), baseFolderName, docPaths)
411 name = util.GetTreeID(block.Path)
412 return
413}
414
415func ExportDataInFolder(exportFolder string) (name string, err error) {
416 util.PushEndlessProgress(Conf.Language(65))
417 defer util.ClearPushProgress(100)
418
419 data := filepath.Join(util.WorkspaceDir, "data")
420 if util.ContainerStd == util.Container {
421 // 桌面端检查磁盘可用空间
422
423 dataSize, sizeErr := util.SizeOfDirectory(data)
424 if sizeErr != nil {
425 logging.LogErrorf("get size of data dir [%s] failed: %s", data, sizeErr)
426 err = sizeErr
427 return
428 }
429
430 _, _, tempExportFree := util.GetDiskUsage(util.TempDir)
431 if int64(tempExportFree) < dataSize*2 { // 压缩 zip 文件时需要 data 的两倍空间
432 err = errors.New(fmt.Sprintf(Conf.Language(242), humanize.BytesCustomCeil(tempExportFree, 2), humanize.BytesCustomCeil(uint64(dataSize)*2, 2)))
433 return
434 }
435
436 _, _, targetExportFree := util.GetDiskUsage(exportFolder)
437 if int64(targetExportFree) < dataSize { // 复制 zip 最多需要 data 一样的空间
438 err = errors.New(fmt.Sprintf(Conf.Language(242), humanize.BytesCustomCeil(targetExportFree, 2), humanize.BytesCustomCeil(uint64(dataSize), 2)))
439 return
440 }
441 }
442
443 zipPath, err := ExportData()
444 if err != nil {
445 return
446 }
447 name = filepath.Base(zipPath)
448 name, err = url.PathUnescape(name)
449 if err != nil {
450 logging.LogErrorf("url unescape [%s] failed: %s", name, err)
451 return
452 }
453
454 util.PushEndlessProgress(Conf.Language(65))
455 defer util.ClearPushProgress(100)
456
457 targetZipPath := filepath.Join(exportFolder, name)
458 zipAbsPath := filepath.Join(util.TempDir, "export", name)
459 err = filelock.Copy(zipAbsPath, targetZipPath)
460 if err != nil {
461 logging.LogErrorf("copy export zip from [%s] to [%s] failed: %s", zipAbsPath, targetZipPath, err)
462 return
463 }
464 if removeErr := os.Remove(zipAbsPath); nil != removeErr {
465 logging.LogErrorf("remove export zip failed: %s", removeErr)
466 }
467 return
468}
469
470func ExportData() (zipPath string, err error) {
471 util.PushEndlessProgress(Conf.Language(65))
472 defer util.ClearPushProgress(100)
473
474 name := util.FilterFileName(util.WorkspaceName) + "-" + util.CurrentTimeSecondsStr()
475 exportFolder := filepath.Join(util.TempDir, "export", name)
476 zipPath, err = exportData(exportFolder)
477 if err != nil {
478 return
479 }
480 zipPath = "/export/" + url.PathEscape(filepath.Base(zipPath))
481 return
482}
483
484func exportData(exportFolder string) (zipPath string, err error) {
485 FlushTxQueue()
486
487 logging.LogInfof("exporting data...")
488
489 baseFolderName := "data-" + util.CurrentTimeSecondsStr()
490 if err = os.MkdirAll(exportFolder, 0755); err != nil {
491 logging.LogErrorf("create export temp folder failed: %s", err)
492 return
493 }
494
495 data := filepath.Join(util.WorkspaceDir, "data")
496 if err = filelock.Copy(data, exportFolder); err != nil {
497 logging.LogErrorf("copy data dir from [%s] to [%s] failed: %s", data, baseFolderName, err)
498 err = errors.New(fmt.Sprintf(Conf.Language(14), err.Error()))
499 return
500 }
501
502 zipPath = exportFolder + ".zip"
503 zip, err := gulu.Zip.Create(zipPath)
504 if err != nil {
505 logging.LogErrorf("create export data zip [%s] failed: %s", exportFolder, err)
506 return
507 }
508
509 zipCallback := func(filename string) {
510 util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(253), filename))
511 }
512
513 if err = zip.AddDirectory(baseFolderName, exportFolder, zipCallback); err != nil {
514 logging.LogErrorf("create export data zip [%s] failed: %s", exportFolder, err)
515 return
516 }
517
518 if err = zip.Close(); err != nil {
519 logging.LogErrorf("close export data zip failed: %s", err)
520 }
521
522 os.RemoveAll(exportFolder)
523 logging.LogInfof("export data done [%s]", zipPath)
524 return
525}
526
527func ExportResources(resourcePaths []string, mainName string) (exportFilePath string, err error) {
528 FlushTxQueue()
529
530 // 用于导出的临时文件夹完整路径
531 exportFolderPath := filepath.Join(util.TempDir, "export", mainName)
532 if err = os.MkdirAll(exportFolderPath, 0755); err != nil {
533 logging.LogErrorf("create export temp folder failed: %s", err)
534 return
535 }
536
537 // 将需要导出的文件/文件夹复制到临时文件夹
538 for _, resourcePath := range resourcePaths {
539 resourceFullPath := filepath.Join(util.WorkspaceDir, resourcePath) // 资源完整路径
540 if !util.IsAbsPathInWorkspace(resourceFullPath) {
541 logging.LogErrorf("resource path [%s] is not in workspace", resourceFullPath)
542 err = errors.New("resource path [" + resourcePath + "] is not in workspace")
543 return
544 }
545
546 resourceBaseName := filepath.Base(resourceFullPath) // 资源名称
547 resourceCopyPath := filepath.Join(exportFolderPath, resourceBaseName) // 资源副本完整路径
548 if err = filelock.Copy(resourceFullPath, resourceCopyPath); err != nil {
549 logging.LogErrorf("copy resource will be exported from [%s] to [%s] failed: %s", resourcePath, resourceCopyPath, err)
550 err = fmt.Errorf(Conf.Language(14), err.Error())
551 return
552 }
553 }
554
555 zipFilePath := exportFolderPath + ".zip" // 导出的 *.zip 文件完整路径
556 zip, err := gulu.Zip.Create(zipFilePath)
557 if err != nil {
558 logging.LogErrorf("create export zip [%s] failed: %s", zipFilePath, err)
559 return
560 }
561
562 if err = zip.AddDirectory(mainName, exportFolderPath); err != nil {
563 logging.LogErrorf("create export zip [%s] failed: %s", exportFolderPath, err)
564 return
565 }
566
567 if err = zip.Close(); err != nil {
568 logging.LogErrorf("close export zip failed: %s", err)
569 }
570
571 os.RemoveAll(exportFolderPath)
572
573 exportFilePath = path.Join("temp", "export", mainName+".zip") // 导出的 *.zip 文件相对于工作区目录的路径
574 return
575}
576
577func Preview(id string, fillCSSVar bool) (retStdHTML string) {
578 blockRefMode := Conf.Export.BlockRefMode
579 bt := treenode.GetBlockTree(id)
580 if nil == bt {
581 return
582 }
583
584 tree := prepareExportTree(bt)
585 tree = exportTree(tree, false, false, true,
586 blockRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode,
587 "#", "#", // 这里固定使用 # 包裹标签,否则无法正确解析标签 https://github.com/siyuan-note/siyuan/issues/13857
588 Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight,
589 Conf.Export.AddTitle, Conf.Export.InlineMemo, true, true, map[string]*parse.Tree{})
590 luteEngine := NewLute()
591 enableLuteInlineSyntax(luteEngine)
592 luteEngine.SetFootnotes(true)
593 addBlockIALNodes(tree, false)
594
595 adjustHeadingLevel(bt, tree)
596
597 // 移除超级块的属性列表 https://github.com/siyuan-note/siyuan/issues/13451
598 var unlinks []*ast.Node
599 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
600 if entering && ast.NodeKramdownBlockIAL == n.Type && nil != n.Previous && ast.NodeSuperBlock == n.Previous.Type {
601 unlinks = append(unlinks, n)
602 }
603 return ast.WalkContinue
604 })
605 for _, unlink := range unlinks {
606 unlink.Unlink()
607 }
608
609 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
610 if !entering {
611 return ast.WalkContinue
612 }
613
614 if ast.NodeFootnotesRef == n.Type && nil != n.Next {
615 // https://github.com/siyuan-note/siyuan/issues/15654
616 nextText := n.NextNodeText()
617 if strings.HasPrefix(nextText, "(") && strings.HasSuffix(nextText, ")") {
618 n.InsertAfter(&ast.Node{Type: ast.NodeText, Tokens: []byte(editor.Zwsp)})
619 }
620 }
621 return ast.WalkContinue
622 })
623
624 md := treenode.FormatNode(tree.Root, luteEngine)
625 tree = parse.Parse("", []byte(md), luteEngine.ParseOptions)
626 // 使用实际主题样式值替换样式变量 Use real theme style value replace var in preview mode https://github.com/siyuan-note/siyuan/issues/11458
627 if fillCSSVar {
628 fillThemeStyleVar(tree)
629 }
630 luteEngine.RenderOptions.ProtyleMarkNetImg = false
631 retStdHTML = luteEngine.ProtylePreview(tree, luteEngine.RenderOptions)
632
633 if footnotesDefBlock := tree.Root.ChildByType(ast.NodeFootnotesDefBlock); nil != footnotesDefBlock {
634 footnotesDefBlock.Unlink()
635 }
636 return
637}
638
639func ExportDocx(id, savePath string, removeAssets, merge bool) (fullPath string, err error) {
640 if !util.IsValidPandocBin(Conf.Export.PandocBin) {
641 Conf.Export.PandocBin = util.PandocBinPath
642 Conf.Save()
643 if !util.IsValidPandocBin(Conf.Export.PandocBin) {
644 err = errors.New(Conf.Language(115))
645 return
646 }
647 }
648
649 tmpDir := filepath.Join(util.TempDir, "export", gulu.Rand.String(7))
650 if err = os.MkdirAll(tmpDir, 0755); err != nil {
651 return
652 }
653 defer os.Remove(tmpDir)
654 name, content := ExportMarkdownHTML(id, tmpDir, true, merge)
655 content = strings.ReplaceAll(content, " \n", "<br>\n")
656
657 tmpDocxPath := filepath.Join(tmpDir, name+".docx")
658 args := []string{ // pandoc -f html --resource-path=请从这里开始 请从这里开始\index.html -o test.docx
659 "-f", "html+tex_math_dollars",
660 "--resource-path", tmpDir,
661 "-o", tmpDocxPath,
662 }
663
664 // Pandoc template for exporting docx https://github.com/siyuan-note/siyuan/issues/8740
665 docxTemplate := util.RemoveInvalid(Conf.Export.DocxTemplate)
666 docxTemplate = strings.TrimSpace(docxTemplate)
667 if "" != docxTemplate {
668 if !gulu.File.IsExist(docxTemplate) {
669 logging.LogErrorf("docx template [%s] not found", docxTemplate)
670 err = errors.New(fmt.Sprintf(Conf.Language(197), docxTemplate))
671 return
672 }
673
674 args = append(args, "--reference-doc", docxTemplate)
675 }
676
677 pandoc := exec.Command(Conf.Export.PandocBin, args...)
678 gulu.CmdAttr(pandoc)
679 pandoc.Stdin = bytes.NewBufferString(content)
680 output, err := pandoc.CombinedOutput()
681 if err != nil {
682 logging.LogErrorf("export docx failed: %s", gulu.Str.FromBytes(output))
683 err = errors.New(fmt.Sprintf(Conf.Language(14), gulu.Str.FromBytes(output)))
684 return
685 }
686
687 fullPath = filepath.Join(savePath, name+".docx")
688 fullPath = util.GetUniqueFilename(fullPath)
689 if err = filelock.Copy(tmpDocxPath, fullPath); err != nil {
690 logging.LogErrorf("export docx failed: %s", err)
691 err = errors.New(fmt.Sprintf(Conf.Language(14), err))
692 return
693 }
694
695 if tmpAssets := filepath.Join(tmpDir, "assets"); !removeAssets && gulu.File.IsDir(tmpAssets) {
696 if err = filelock.Copy(tmpAssets, filepath.Join(savePath, "assets")); err != nil {
697 logging.LogErrorf("export docx failed: %s", err)
698 err = errors.New(fmt.Sprintf(Conf.Language(14), err))
699 return
700 }
701 }
702 return
703}
704
705func ExportMarkdownHTML(id, savePath string, docx, merge bool) (name, dom string) {
706 bt := treenode.GetBlockTree(id)
707 if nil == bt {
708 return
709 }
710
711 tree := prepareExportTree(bt)
712
713 if merge {
714 var mergeErr error
715 tree, mergeErr = mergeSubDocs(tree)
716 if nil != mergeErr {
717 logging.LogErrorf("merge sub docs failed: %s", mergeErr)
718 return
719 }
720 }
721
722 blockRefMode := Conf.Export.BlockRefMode
723 tree = exportTree(tree, true, false, true,
724 blockRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode,
725 Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker,
726 Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight,
727 Conf.Export.AddTitle, Conf.Export.InlineMemo, true, true, map[string]*parse.Tree{})
728 name = path.Base(tree.HPath)
729 name = util.FilterFileName(name) // 导出 PDF、HTML 和 Word 时未移除不支持的文件名符号 https://github.com/siyuan-note/siyuan/issues/5614
730 savePath = strings.TrimSpace(savePath)
731
732 if err := os.MkdirAll(savePath, 0755); err != nil {
733 logging.LogErrorf("mkdir [%s] failed: %s", savePath, err)
734 return
735 }
736
737 assets := getAssetsLinkDests(tree.Root)
738 for _, asset := range assets {
739 if strings.HasPrefix(asset, "assets/") {
740 if strings.Contains(asset, "?") {
741 asset = asset[:strings.LastIndex(asset, "?")]
742 }
743
744 srcAbsPath, err := GetAssetAbsPath(asset)
745 if err != nil {
746 logging.LogWarnf("resolve path of asset [%s] failed: %s", asset, err)
747 continue
748 }
749 targetAbsPath := filepath.Join(savePath, asset)
750 if err = filelock.Copy(srcAbsPath, targetAbsPath); err != nil {
751 logging.LogWarnf("copy asset from [%s] to [%s] failed: %s", srcAbsPath, targetAbsPath, err)
752 }
753 }
754 }
755
756 srcs := []string{"stage/build/export", "stage/protyle"}
757 for _, src := range srcs {
758 from := filepath.Join(util.WorkingDir, src)
759 to := filepath.Join(savePath, src)
760 if err := filelock.Copy(from, to); err != nil {
761 logging.LogWarnf("copy stage from [%s] to [%s] failed: %s", from, savePath, err)
762 }
763 }
764
765 theme := Conf.Appearance.ThemeLight
766 if 1 == Conf.Appearance.Mode {
767 theme = Conf.Appearance.ThemeDark
768 }
769 // 复制主题文件夹
770 srcs = []string{"themes/" + theme}
771 appearancePath := util.AppearancePath
772 if util.IsSymlinkPath(util.AppearancePath) {
773 // Support for symlinked theme folder when exporting HTML https://github.com/siyuan-note/siyuan/issues/9173
774 var readErr error
775 appearancePath, readErr = filepath.EvalSymlinks(util.AppearancePath)
776 if nil != readErr {
777 logging.LogErrorf("readlink [%s] failed: %s", util.AppearancePath, readErr)
778 return
779 }
780 }
781
782 for _, src := range srcs {
783 from := filepath.Join(appearancePath, src)
784 to := filepath.Join(savePath, "appearance", src)
785 if err := filelock.Copy(from, to); err != nil {
786 logging.LogErrorf("copy appearance from [%s] to [%s] failed: %s", from, savePath, err)
787 return
788 }
789 }
790
791 // 只复制图标文件夹中的 icon.js 文件
792 iconName := Conf.Appearance.Icon
793 // 如果使用的不是内建图标(ant 或 material),需要复制 material 作为后备
794 if iconName != "ant" && iconName != "material" && iconName != "" {
795 srcIconFile := filepath.Join(appearancePath, "icons", "material", "icon.js")
796 toIconDir := filepath.Join(savePath, "appearance", "icons", "material")
797 if err := os.MkdirAll(toIconDir, 0755); err != nil {
798 logging.LogErrorf("mkdir [%s] failed: %s", toIconDir, err)
799 return
800 }
801 toIconFile := filepath.Join(toIconDir, "icon.js")
802 if err := filelock.Copy(srcIconFile, toIconFile); err != nil {
803 logging.LogWarnf("copy icon file from [%s] to [%s] failed: %s", srcIconFile, toIconFile, err)
804 }
805 }
806 // 复制当前使用的图标文件
807 if iconName != "" {
808 srcIconFile := filepath.Join(appearancePath, "icons", iconName, "icon.js")
809 toIconDir := filepath.Join(savePath, "appearance", "icons", iconName)
810 if err := os.MkdirAll(toIconDir, 0755); err != nil {
811 logging.LogErrorf("mkdir [%s] failed: %s", toIconDir, err)
812 return
813 }
814 toIconFile := filepath.Join(toIconDir, "icon.js")
815 if err := filelock.Copy(srcIconFile, toIconFile); err != nil {
816 logging.LogWarnf("copy icon file from [%s] to [%s] failed: %s", srcIconFile, toIconFile, err)
817 }
818 }
819
820 // 复制自定义表情图片
821 emojis := emojisInTree(tree)
822 for _, emoji := range emojis {
823 from := filepath.Join(util.DataDir, emoji)
824 to := filepath.Join(savePath, emoji)
825 if err := filelock.Copy(from, to); err != nil {
826 logging.LogErrorf("copy emojis from [%s] to [%s] failed: %s", from, to, err)
827 }
828 }
829
830 if docx {
831 processIFrame(tree)
832 }
833
834 luteEngine := NewLute()
835 luteEngine.SetFootnotes(true)
836
837 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
838 if !entering {
839 return ast.WalkContinue
840 }
841 if ast.NodeEmojiImg == n.Type {
842 // 自定义表情图片地址去掉开头的 /
843 n.Tokens = bytes.ReplaceAll(n.Tokens, []byte("src=\"/emojis"), []byte("src=\"emojis"))
844 } else if ast.NodeList == n.Type {
845 if nil != n.ListData && 1 == n.ListData.Typ {
846 if 0 == n.ListData.Start {
847 n.ListData.Start = 1
848 }
849 if li := n.ChildByType(ast.NodeListItem); nil != li && nil != li.ListData {
850 n.ListData.Start = li.ListData.Num
851 }
852 }
853 } else if n.IsTextMarkType("code") {
854 if nil != n.Next && ast.NodeText == n.Next.Type {
855 // 行级代码导出 word 之后会有多余的零宽空格 https://github.com/siyuan-note/siyuan/issues/14825
856 n.Next.Tokens = bytes.TrimPrefix(n.Next.Tokens, []byte(editor.Zwsp))
857 }
858 }
859 return ast.WalkContinue
860 })
861
862 if docx {
863 renderer := render.NewProtyleExportDocxRenderer(tree, luteEngine.RenderOptions)
864 output := renderer.Render()
865 dom = gulu.Str.FromBytes(output)
866 } else {
867 dom = luteEngine.ProtylePreview(tree, luteEngine.RenderOptions)
868 }
869 return
870}
871
872func ExportHTML(id, savePath string, pdf, image, keepFold, merge bool) (name, dom string, node *ast.Node) {
873 savePath = strings.TrimSpace(savePath)
874
875 bt := treenode.GetBlockTree(id)
876 if nil == bt {
877 return
878 }
879
880 tree := prepareExportTree(bt)
881 node = treenode.GetNodeInTree(tree, id)
882
883 if merge {
884 var mergeErr error
885 tree, mergeErr = mergeSubDocs(tree)
886 if nil != mergeErr {
887 logging.LogErrorf("merge sub docs failed: %s", mergeErr)
888 return
889 }
890 }
891
892 blockRefMode := Conf.Export.BlockRefMode
893 var headings []*ast.Node
894 if pdf { // 导出 PDF 需要标记目录书签
895 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
896 if entering && ast.NodeHeading == n.Type && !n.ParentIs(ast.NodeBlockquote) {
897 headings = append(headings, n)
898 return ast.WalkSkipChildren
899 }
900 return ast.WalkContinue
901 })
902
903 for _, h := range headings {
904 link := &ast.Node{Type: ast.NodeLink}
905 link.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
906 link.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(" ")})
907 link.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
908 link.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
909 link.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(PdfOutlineScheme + "://" + h.ID)})
910 link.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
911 h.PrependChild(link)
912 }
913 }
914
915 tree = exportTree(tree, true, keepFold, true,
916 blockRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode,
917 Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker,
918 Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight,
919 Conf.Export.AddTitle, Conf.Export.InlineMemo, true, true, map[string]*parse.Tree{})
920 adjustHeadingLevel(bt, tree)
921 name = path.Base(tree.HPath)
922 name = util.FilterFileName(name) // 导出 PDF、HTML 和 Word 时未移除不支持的文件名符号 https://github.com/siyuan-note/siyuan/issues/5614
923
924 if "" != savePath {
925 if err := os.MkdirAll(savePath, 0755); err != nil {
926 logging.LogErrorf("mkdir [%s] failed: %s", savePath, err)
927 return
928 }
929
930 assets := getAssetsLinkDests(tree.Root)
931 for _, asset := range assets {
932 if strings.Contains(asset, "?") {
933 asset = asset[:strings.LastIndex(asset, "?")]
934 }
935
936 srcAbsPath, err := GetAssetAbsPath(asset)
937 if err != nil {
938 logging.LogWarnf("resolve path of asset [%s] failed: %s", asset, err)
939 continue
940 }
941 targetAbsPath := filepath.Join(savePath, asset)
942 if err = filelock.Copy(srcAbsPath, targetAbsPath); err != nil {
943 logging.LogWarnf("copy asset from [%s] to [%s] failed: %s", srcAbsPath, targetAbsPath, err)
944 }
945 }
946 }
947
948 if !pdf && "" != savePath { // 导出 HTML 需要复制静态资源
949 srcs := []string{"stage/build/export", "stage/protyle"}
950 for _, src := range srcs {
951 from := filepath.Join(util.WorkingDir, src)
952 to := filepath.Join(savePath, src)
953 if err := filelock.Copy(from, to); err != nil {
954 logging.LogErrorf("copy stage from [%s] to [%s] failed: %s", from, savePath, err)
955 return
956 }
957 }
958
959 theme := Conf.Appearance.ThemeLight
960 if 1 == Conf.Appearance.Mode {
961 theme = Conf.Appearance.ThemeDark
962 }
963 // 复制主题文件夹
964 srcs = []string{"themes/" + theme}
965 appearancePath := util.AppearancePath
966 if util.IsSymlinkPath(util.AppearancePath) {
967 // Support for symlinked theme folder when exporting HTML https://github.com/siyuan-note/siyuan/issues/9173
968 var readErr error
969 appearancePath, readErr = filepath.EvalSymlinks(util.AppearancePath)
970 if nil != readErr {
971 logging.LogErrorf("readlink [%s] failed: %s", util.AppearancePath, readErr)
972 return
973 }
974 }
975 for _, src := range srcs {
976 from := filepath.Join(appearancePath, src)
977 to := filepath.Join(savePath, "appearance", src)
978 if err := filelock.Copy(from, to); err != nil {
979 logging.LogErrorf("copy appearance from [%s] to [%s] failed: %s", from, savePath, err)
980 }
981 }
982
983 // 只复制图标文件夹中的 icon.js 文件
984 iconName := Conf.Appearance.Icon
985 // 如果使用的不是内建图标(ant 或 material),需要复制 material 作为后备
986 if iconName != "ant" && iconName != "material" && iconName != "" {
987 srcIconFile := filepath.Join(appearancePath, "icons", "material", "icon.js")
988 toIconDir := filepath.Join(savePath, "appearance", "icons", "material")
989 if err := os.MkdirAll(toIconDir, 0755); err != nil {
990 logging.LogErrorf("mkdir [%s] failed: %s", toIconDir, err)
991 return
992 }
993 toIconFile := filepath.Join(toIconDir, "icon.js")
994 if err := filelock.Copy(srcIconFile, toIconFile); err != nil {
995 logging.LogWarnf("copy icon file from [%s] to [%s] failed: %s", srcIconFile, toIconFile, err)
996 }
997 }
998 // 复制当前使用的图标文件
999 if iconName != "" {
1000 srcIconFile := filepath.Join(appearancePath, "icons", iconName, "icon.js")
1001 toIconDir := filepath.Join(savePath, "appearance", "icons", iconName)
1002 if err := os.MkdirAll(toIconDir, 0755); err != nil {
1003 logging.LogErrorf("mkdir [%s] failed: %s", toIconDir, err)
1004 return
1005 }
1006 toIconFile := filepath.Join(toIconDir, "icon.js")
1007 if err := filelock.Copy(srcIconFile, toIconFile); err != nil {
1008 logging.LogWarnf("copy icon file from [%s] to [%s] failed: %s", srcIconFile, toIconFile, err)
1009 }
1010 }
1011
1012 // 复制自定义表情图片
1013 emojis := emojisInTree(tree)
1014 for _, emoji := range emojis {
1015 from := filepath.Join(util.DataDir, emoji)
1016 to := filepath.Join(savePath, emoji)
1017 if err := filelock.Copy(from, to); err != nil {
1018 logging.LogErrorf("copy emojis from [%s] to [%s] failed: %s", from, to, err)
1019 }
1020 }
1021 }
1022
1023 if pdf {
1024 processIFrame(tree)
1025 }
1026
1027 luteEngine := NewLute()
1028 luteEngine.SetFootnotes(true)
1029 luteEngine.RenderOptions.ProtyleContenteditable = false
1030 luteEngine.SetProtyleMarkNetImg(false)
1031
1032 // 不进行安全过滤,因为导出时需要保留所有的 HTML 标签
1033 // 使用属性 `data-export-html` 导出时 `<style></style>` 标签丢失 https://github.com/siyuan-note/siyuan/issues/6228
1034 luteEngine.SetSanitize(false)
1035
1036 renderer := render.NewProtyleExportRenderer(tree, luteEngine.RenderOptions)
1037 dom = gulu.Str.FromBytes(renderer.Render())
1038 return
1039}
1040
1041func prepareExportTree(bt *treenode.BlockTree) (ret *parse.Tree) {
1042 luteEngine := NewLute()
1043 ret, _ = filesys.LoadTree(bt.BoxID, bt.Path, luteEngine)
1044 if "d" != bt.Type {
1045 node := treenode.GetNodeInTree(ret, bt.ID)
1046 nodes := []*ast.Node{node}
1047 if "h" == bt.Type {
1048 children := treenode.HeadingChildren(node)
1049 for _, child := range children {
1050 nodes = append(nodes, child)
1051 }
1052 }
1053
1054 oldRoot := ret.Root
1055 ret = parse.Parse("", []byte(""), luteEngine.ParseOptions)
1056 first := ret.Root.FirstChild
1057 for _, n := range nodes {
1058 first.InsertBefore(n)
1059 }
1060 ret.Root.KramdownIAL = oldRoot.KramdownIAL
1061 }
1062 ret.Path = bt.Path
1063 ret.HPath = bt.HPath
1064 ret.Box = bt.BoxID
1065 ret.ID = bt.RootID
1066 return
1067}
1068
1069func processIFrame(tree *parse.Tree) {
1070 // 导出 PDF/Word 时 IFrame 块使用超链接 https://github.com/siyuan-note/siyuan/issues/4035
1071 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
1072 if !entering || ast.NodeIFrame != n.Type {
1073 return ast.WalkContinue
1074 }
1075
1076 n.Type = ast.NodeParagraph
1077 index := bytes.Index(n.Tokens, []byte("src=\""))
1078 if 0 > index {
1079 n.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: n.Tokens})
1080 } else {
1081 src := n.Tokens[index+len("src=\""):]
1082 src = src[:bytes.Index(src, []byte("\""))]
1083 src = html.UnescapeHTML(src)
1084 link := &ast.Node{Type: ast.NodeLink}
1085 link.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
1086 link.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: src})
1087 link.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
1088 link.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
1089 link.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: src})
1090 link.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
1091 n.AppendChild(link)
1092 }
1093 return ast.WalkContinue
1094 })
1095}
1096
1097func ProcessPDF(id, p string, merge, removeAssets, watermark bool) (err error) {
1098 tree, _ := LoadTreeByBlockID(id)
1099 if nil == tree {
1100 return
1101 }
1102
1103 if merge {
1104 var mergeErr error
1105 tree, mergeErr = mergeSubDocs(tree)
1106 if nil != mergeErr {
1107 logging.LogErrorf("merge sub docs failed: %s", mergeErr)
1108 return
1109 }
1110 }
1111
1112 var headings []*ast.Node
1113 assetDests := getAssetsLinkDests(tree.Root)
1114 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
1115 if !entering {
1116 return ast.WalkContinue
1117 }
1118
1119 if ast.NodeHeading == n.Type && !n.ParentIs(ast.NodeBlockquote) {
1120 headings = append(headings, n)
1121 return ast.WalkSkipChildren
1122 }
1123 return ast.WalkContinue
1124 })
1125
1126 api.DisableConfigDir()
1127 font.UserFontDir = filepath.Join(util.HomeDir, ".config", "siyuan", "fonts")
1128 if mkdirErr := os.MkdirAll(font.UserFontDir, 0755); nil != mkdirErr {
1129 logging.LogErrorf("mkdir [%s] failed: %s", font.UserFontDir, mkdirErr)
1130 return
1131 }
1132 if loadErr := api.LoadUserFonts(); nil != loadErr {
1133 logging.LogErrorf("load user fonts failed: %s", loadErr)
1134 }
1135
1136 pdfCtx, ctxErr := api.ReadContextFile(p)
1137 if nil != ctxErr {
1138 logging.LogErrorf("read pdf context failed: %s", ctxErr)
1139 return
1140 }
1141
1142 processPDFBookmarks(pdfCtx, headings)
1143 processPDFLinkEmbedAssets(pdfCtx, assetDests, removeAssets)
1144 processPDFWatermark(pdfCtx, watermark)
1145
1146 pdfcpuVer := model.VersionStr
1147 model.VersionStr = "SiYuan v" + util.Ver + " (pdfcpu " + pdfcpuVer + ")"
1148 if writeErr := api.WriteContextFile(pdfCtx, p); nil != writeErr {
1149 logging.LogErrorf("write pdf context failed: %s", writeErr)
1150 return
1151 }
1152 return
1153}
1154
1155func processPDFWatermark(pdfCtx *model.Context, watermark bool) {
1156 // Support adding the watermark on export PDF https://github.com/siyuan-note/siyuan/issues/9961
1157 // https://pdfcpu.io/core/watermark
1158
1159 if !watermark {
1160 return
1161 }
1162
1163 str := Conf.Export.PDFWatermarkStr
1164 if "" == str {
1165 return
1166 }
1167
1168 mode := "text"
1169 if gulu.File.IsExist(str) {
1170 if ".pdf" == strings.ToLower(filepath.Ext(str)) {
1171 mode = "pdf"
1172 } else {
1173 mode = "image"
1174 }
1175 }
1176
1177 desc := Conf.Export.PDFWatermarkDesc
1178 if "text" == mode && util.ContainsCJK(str) {
1179 // 中日韩文本水印需要安装字体文件
1180 descParts := strings.Split(desc, ",")
1181 m := map[string]string{}
1182 for _, descPart := range descParts {
1183 kv := strings.Split(descPart, ":")
1184 if 2 != len(kv) {
1185 continue
1186 }
1187 m[kv[0]] = kv[1]
1188 }
1189
1190 useDefaultFont := true
1191 if "" != m["fontname"] {
1192 listFonts, e := api.ListFonts()
1193 var builtInFontNames []string
1194 if nil != e {
1195 logging.LogInfof("listFont failed: %s", e)
1196 } else {
1197 for _, f := range listFonts {
1198 if strings.Contains(f, "(") {
1199 f = f[:strings.Index(f, "(")]
1200 }
1201 f = strings.TrimSpace(f)
1202 if strings.Contains(f, ":") || "" == f || strings.Contains(f, "Corefonts") || strings.Contains(f, "Userfonts") {
1203 continue
1204 }
1205
1206 builtInFontNames = append(builtInFontNames, f)
1207 }
1208
1209 for _, font := range builtInFontNames {
1210 if font == m["fontname"] {
1211 useDefaultFont = false
1212 break
1213 }
1214 }
1215 }
1216 }
1217 if useDefaultFont {
1218 m["fontname"] = "LXGWWenKaiLite-Regular"
1219 fontPath := filepath.Join(util.AppearancePath, "fonts", "LxgwWenKai-Lite-1.501", "LXGWWenKaiLite-Regular.ttf")
1220 err := api.InstallFonts([]string{fontPath})
1221 if err != nil {
1222 logging.LogErrorf("install font [%s] failed: %s", fontPath, err)
1223 }
1224 }
1225
1226 descBuilder := bytes.Buffer{}
1227 for k, v := range m {
1228 descBuilder.WriteString(k)
1229 descBuilder.WriteString(":")
1230 descBuilder.WriteString(v)
1231 descBuilder.WriteString(",")
1232 }
1233 desc = descBuilder.String()
1234 desc = desc[:len(desc)-1]
1235 }
1236
1237 logging.LogInfof("add PDF watermark [mode=%s, str=%s, desc=%s]", mode, str, desc)
1238
1239 var wm *model.Watermark
1240 var err error
1241 switch mode {
1242 case "text":
1243 wm, err = pdfcpu.ParseTextWatermarkDetails(str, desc, false, types.POINTS)
1244 case "image":
1245 wm, err = pdfcpu.ParseImageWatermarkDetails(str, desc, false, types.POINTS)
1246 case "pdf":
1247 wm, err = pdfcpu.ParsePDFWatermarkDetails(str, desc, false, types.POINTS)
1248 }
1249
1250 if err != nil {
1251 logging.LogErrorf("parse watermark failed: %s", err)
1252 util.PushErrMsg(err.Error(), 7000)
1253 return
1254 }
1255
1256 wm.OnTop = true // Export PDF and add watermarks no longer covered by images https://github.com/siyuan-note/siyuan/issues/10818
1257 err = pdfcpu.AddWatermarks(pdfCtx, nil, wm)
1258 if err != nil {
1259 logging.LogErrorf("add watermark failed: %s", err)
1260 return
1261 }
1262}
1263
1264func processPDFBookmarks(pdfCtx *model.Context, headings []*ast.Node) {
1265 links, err := PdfListToCLinks(pdfCtx)
1266 if err != nil {
1267 return
1268 }
1269
1270 sort.Slice(links, func(i, j int) bool {
1271 return links[i].Page < links[j].Page
1272 })
1273
1274 titles := map[string]bool{}
1275 bms := map[string]*pdfcpu.Bookmark{}
1276 for _, link := range links {
1277 linkID := link.URI[strings.LastIndex(link.URI, "/")+1:]
1278 b := sql.GetBlock(linkID)
1279 if nil == b {
1280 logging.LogWarnf("pdf outline block [%s] not found", linkID)
1281 continue
1282 }
1283 title := b.Content
1284 title, _ = url.QueryUnescape(title)
1285 for {
1286 if _, ok := titles[title]; ok {
1287 title += "\x01"
1288 } else {
1289 titles[title] = true
1290 break
1291 }
1292 }
1293 bm := &pdfcpu.Bookmark{
1294 Title: title,
1295 PageFrom: link.Page,
1296 AbsPos: link.Rect.UR.Y,
1297 }
1298 bms[linkID] = bm
1299 }
1300
1301 if 1 > len(bms) {
1302 return
1303 }
1304
1305 var topBms []*pdfcpu.Bookmark
1306 stack := linkedliststack.New()
1307 for _, h := range headings {
1308 L:
1309 for ; ; stack.Pop() {
1310 cur, ok := stack.Peek()
1311 if !ok {
1312 bm, ok := bms[h.ID]
1313 if !ok {
1314 break L
1315 }
1316 bm.Level = h.HeadingLevel
1317 stack.Push(bm)
1318 topBms = append(topBms, bm)
1319 break L
1320 }
1321
1322 tip := cur.(*pdfcpu.Bookmark)
1323 if tip.Level < h.HeadingLevel {
1324 bm := bms[h.ID]
1325 bm.Level = h.HeadingLevel
1326 bm.Parent = tip
1327 tip.Kids = append(tip.Kids, bm)
1328 stack.Push(bm)
1329 break L
1330 }
1331 }
1332 }
1333
1334 err = pdfcpu.AddBookmarks(pdfCtx, topBms, true)
1335 if err != nil {
1336 logging.LogErrorf("add bookmark failed: %s", err)
1337 return
1338 }
1339}
1340
1341// processPDFLinkEmbedAssets 处理资源文件超链接,根据 removeAssets 参数决定是否将资源文件嵌入到 PDF 中。
1342// 导出 PDF 时支持将资源文件作为附件嵌入 https://github.com/siyuan-note/siyuan/issues/7414
1343func processPDFLinkEmbedAssets(pdfCtx *model.Context, assetDests []string, removeAssets bool) {
1344 var assetAbsPaths []string
1345 for _, dest := range assetDests {
1346 if absPath, _ := GetAssetAbsPath(dest); "" != absPath {
1347 assetAbsPaths = append(assetAbsPaths, absPath)
1348 }
1349 }
1350
1351 if 1 > len(assetAbsPaths) {
1352 return
1353 }
1354
1355 assetLinks, otherLinks, listErr := PdfListLinks(pdfCtx)
1356 if nil != listErr {
1357 logging.LogErrorf("list asset links failed: %s", listErr)
1358 return
1359 }
1360
1361 if 1 > len(assetLinks) {
1362 return
1363 }
1364
1365 if _, removeErr := pdfcpu.RemoveAnnotations(pdfCtx, nil, nil, nil, false); nil != removeErr {
1366 logging.LogWarnf("remove annotations failed: %s", removeErr)
1367 }
1368
1369 linkMap := map[int][]model.AnnotationRenderer{}
1370 for _, link := range otherLinks {
1371 link.URI, _ = url.PathUnescape(link.URI)
1372 if 1 > len(linkMap[link.Page]) {
1373 linkMap[link.Page] = []model.AnnotationRenderer{link}
1374 } else {
1375 linkMap[link.Page] = append(linkMap[link.Page], link)
1376 }
1377 }
1378
1379 attachmentMap := map[int][]*types.IndirectRef{}
1380 now := types.StringLiteral(types.DateString(time.Now()))
1381 for _, link := range assetLinks {
1382 link.URI = strings.ReplaceAll(link.URI, "http://"+util.LocalHost+":"+util.ServerPort+"/export/temp/", "")
1383 link.URI = strings.ReplaceAll(link.URI, "http://"+util.LocalHost+":"+util.ServerPort+"/", "") // Exporting PDF embedded asset files as attachments fails https://github.com/siyuan-note/siyuan/issues/7414#issuecomment-1704573557
1384 link.URI, _ = url.PathUnescape(link.URI)
1385 if idx := strings.Index(link.URI, "?"); 0 < idx {
1386 link.URI = link.URI[:idx]
1387 }
1388
1389 if !removeAssets {
1390 // 不移除资源文件夹的话将超链接指向资源文件夹
1391 if 1 > len(linkMap[link.Page]) {
1392 linkMap[link.Page] = []model.AnnotationRenderer{link}
1393 } else {
1394 linkMap[link.Page] = append(linkMap[link.Page], link)
1395 }
1396
1397 continue
1398 }
1399
1400 // 移除资源文件夹的话使用内嵌附件
1401
1402 absPath, getErr := GetAssetAbsPath(link.URI)
1403 if nil != getErr {
1404 continue
1405 }
1406
1407 ir, newErr := pdfCtx.XRefTable.NewEmbeddedFileStreamDict(absPath)
1408 if nil != newErr {
1409 logging.LogWarnf("new embedded file stream dict failed: %s", newErr)
1410 continue
1411 }
1412
1413 fn := filepath.Base(absPath)
1414 fileSpecDict, newErr := pdfCtx.XRefTable.NewFileSpecDict(fn, fn, "attached by SiYuan", *ir)
1415 if nil != newErr {
1416 logging.LogWarnf("new file spec dict failed: %s", newErr)
1417 continue
1418 }
1419
1420 ir, indErr := pdfCtx.XRefTable.IndRefForNewObject(fileSpecDict)
1421 if nil != indErr {
1422 logging.LogWarnf("ind ref for new object failed: %s", indErr)
1423 continue
1424 }
1425
1426 lx := link.Rect.LL.X + link.Rect.Width()
1427 ly := link.Rect.LL.Y + link.Rect.Height()/2
1428 w := link.Rect.Height() / 2
1429 h := link.Rect.Height() / 2
1430
1431 d := types.Dict(
1432 map[string]types.Object{
1433 "Type": types.Name("Annot"),
1434 "Subtype": types.Name("FileAttachment"),
1435 "Contents": types.StringLiteral(""),
1436 "Rect": types.RectForWidthAndHeight(lx, ly, w, h).Array(),
1437 "P": link.P,
1438 "M": now,
1439 "F": types.Integer(0),
1440 "Border": types.NewIntegerArray(0, 0, 1),
1441 "C": types.NewNumberArray(0.5, 0.0, 0.5),
1442 "CA": types.Float(0.95),
1443 "CreationDate": now,
1444 "Name": types.Name("FileAttachment"),
1445 "FS": *ir,
1446 "NM": types.StringLiteral(""),
1447 },
1448 )
1449
1450 ann, indErr := pdfCtx.XRefTable.IndRefForNewObject(d)
1451 if nil != indErr {
1452 logging.LogWarnf("ind ref for new object failed: %s", indErr)
1453 continue
1454 }
1455
1456 pageDictIndRef, pageErr := pdfCtx.PageDictIndRef(link.Page)
1457 if nil != pageErr {
1458 logging.LogWarnf("page dict ind ref failed: %s", pageErr)
1459 continue
1460 }
1461
1462 d, defErr := pdfCtx.DereferenceDict(*pageDictIndRef)
1463 if nil != defErr {
1464 logging.LogWarnf("dereference dict failed: %s", defErr)
1465 continue
1466 }
1467
1468 if 1 > len(attachmentMap[link.Page]) {
1469 attachmentMap[link.Page] = []*types.IndirectRef{ann}
1470 } else {
1471 attachmentMap[link.Page] = append(attachmentMap[link.Page], ann)
1472 }
1473 }
1474
1475 if 0 < len(linkMap) {
1476 if _, addErr := pdfcpu.AddAnnotationsMap(pdfCtx, linkMap, false); nil != addErr {
1477 logging.LogErrorf("add annotations map failed: %s", addErr)
1478 }
1479 }
1480
1481 // 添加附件注解指向内嵌的附件
1482 for page, anns := range attachmentMap {
1483 pageDictIndRef, pageErr := pdfCtx.PageDictIndRef(page)
1484 if nil != pageErr {
1485 logging.LogWarnf("page dict ind ref failed: %s", pageErr)
1486 continue
1487 }
1488
1489 pageDict, defErr := pdfCtx.DereferenceDict(*pageDictIndRef)
1490 if nil != defErr {
1491 logging.LogWarnf("dereference dict failed: %s", defErr)
1492 continue
1493 }
1494
1495 array := types.Array{}
1496 for _, ann := range anns {
1497 array = append(array, *ann)
1498 }
1499
1500 obj, found := pageDict.Find("Annots")
1501 if !found {
1502 pageDict.Insert("Annots", array)
1503 pdfCtx.EnsureVersionForWriting()
1504 continue
1505 }
1506
1507 ir, ok := obj.(types.IndirectRef)
1508 if !ok {
1509 pageDict.Update("Annots", append(obj.(types.Array), array...))
1510 pdfCtx.EnsureVersionForWriting()
1511 continue
1512 }
1513
1514 // Annots array is an IndirectReference.
1515
1516 o, err := pdfCtx.Dereference(ir)
1517 if err != nil || o == nil {
1518 continue
1519 }
1520
1521 annots, _ := o.(types.Array)
1522 entry, ok := pdfCtx.FindTableEntryForIndRef(&ir)
1523 if !ok {
1524 continue
1525 }
1526 entry.Object = append(annots, array...)
1527 pdfCtx.EnsureVersionForWriting()
1528 }
1529}
1530
1531func ExportStdMarkdown(id string, assetsDestSpace2Underscore, fillCSSVar, adjustHeadingLevel, imgTag bool) string {
1532 bt := treenode.GetBlockTree(id)
1533 if nil == bt {
1534 logging.LogErrorf("block tree [%s] not found", id)
1535 return ""
1536 }
1537
1538 tree := prepareExportTree(bt)
1539 cloudAssetsBase := ""
1540 if IsSubscriber() {
1541 cloudAssetsBase = util.GetCloudAssetsServer() + Conf.GetUser().UserId + "/"
1542 }
1543
1544 var defBlockIDs []string
1545 if 4 == Conf.Export.BlockRefMode { // 脚注+锚点哈希
1546 // 导出锚点哈希,这里先记录下所有定义块的 ID
1547 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
1548 if !entering {
1549 return ast.WalkContinue
1550 }
1551
1552 var defID string
1553 if treenode.IsBlockLink(n) {
1554 defID = strings.TrimPrefix(n.TextMarkAHref, "siyuan://blocks/")
1555 } else if treenode.IsBlockRef(n) {
1556 defID, _, _ = treenode.GetBlockRef(n)
1557 }
1558
1559 if "" != defID {
1560 if defBt := treenode.GetBlockTree(defID); nil != defBt {
1561 defBlockIDs = append(defBlockIDs, defID)
1562 defBlockIDs = gulu.Str.RemoveDuplicatedElem(defBlockIDs)
1563 }
1564 }
1565 return ast.WalkContinue
1566 })
1567 }
1568 defBlockIDs = gulu.Str.RemoveDuplicatedElem(defBlockIDs)
1569
1570 return exportMarkdownContent0(id, tree, cloudAssetsBase, assetsDestSpace2Underscore, adjustHeadingLevel, imgTag,
1571 ".md", Conf.Export.BlockRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode,
1572 Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker,
1573 Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight,
1574 Conf.Export.AddTitle, Conf.Export.InlineMemo, defBlockIDs, true, fillCSSVar, map[string]*parse.Tree{})
1575}
1576
1577func ExportPandocConvertZip(ids []string, pandocTo, ext string) (name, zipPath string) {
1578 block := treenode.GetBlockTree(ids[0])
1579 box := Conf.Box(block.BoxID)
1580 baseFolderName := path.Base(block.HPath)
1581 if "." == baseFolderName {
1582 baseFolderName = path.Base(block.Path)
1583 }
1584
1585 var docPaths []string
1586 bts := treenode.GetBlockTrees(ids)
1587 for _, bt := range bts {
1588 docPaths = append(docPaths, bt.Path)
1589 docFiles := box.ListFiles(strings.TrimSuffix(bt.Path, ".sy"))
1590 for _, docFile := range docFiles {
1591 docPaths = append(docPaths, docFile.path)
1592 }
1593 }
1594
1595 defBlockIDs, trees, docPaths := prepareExportTrees(docPaths)
1596 zipPath = exportPandocConvertZip(baseFolderName, docPaths, defBlockIDs, "gfm+footnotes+hard_line_breaks", pandocTo, ext, trees)
1597 name = util.GetTreeID(block.Path)
1598 return
1599}
1600
1601func ExportNotebookMarkdown(boxID string) (zipPath string) {
1602 util.PushEndlessProgress(Conf.Language(65))
1603 defer util.ClearPushProgress(100)
1604
1605 box := Conf.Box(boxID)
1606 if nil == box {
1607 logging.LogErrorf("not found box [%s]", boxID)
1608 return
1609 }
1610
1611 var docPaths []string
1612 docFiles := box.ListFiles("/")
1613 for _, docFile := range docFiles {
1614 docPaths = append(docPaths, docFile.path)
1615 }
1616
1617 defBlockIDs, trees, docPaths := prepareExportTrees(docPaths)
1618 zipPath = exportPandocConvertZip(box.Name, docPaths, defBlockIDs, "", "", ".md", trees)
1619 return
1620}
1621
1622func yfm(docIAL map[string]string) string {
1623 // 导出 Markdown 文件时开头附上一些元数据 https://github.com/siyuan-note/siyuan/issues/6880
1624
1625 buf := bytes.Buffer{}
1626 buf.WriteString("---\n")
1627 var title, created, updated, tags string
1628 for k, v := range docIAL {
1629 if "id" == k {
1630 createdTime, parseErr := time.Parse("20060102150405", util.TimeFromID(v))
1631 if nil == parseErr {
1632 created = createdTime.Format(time.RFC3339)
1633 }
1634 continue
1635 }
1636 if "title" == k {
1637 title = v
1638 continue
1639 }
1640 if "updated" == k {
1641 updatedTime, parseErr := time.Parse("20060102150405", v)
1642 if nil == parseErr {
1643 updated = updatedTime.Format(time.RFC3339)
1644 }
1645 continue
1646 }
1647 if "tags" == k {
1648 tags = v
1649 continue
1650 }
1651 }
1652 if "" != title {
1653 buf.WriteString("title: ")
1654 buf.WriteString(title)
1655 buf.WriteString("\n")
1656 }
1657 if "" == updated {
1658 updated = time.Now().Format(time.RFC3339)
1659 }
1660 if "" == created {
1661 created = updated
1662 }
1663 buf.WriteString("date: ")
1664 buf.WriteString(created)
1665 buf.WriteString("\n")
1666 buf.WriteString("lastmod: ")
1667 buf.WriteString(updated)
1668 buf.WriteString("\n")
1669 if "" != tags {
1670 buf.WriteString("tags: [")
1671 buf.WriteString(tags)
1672 buf.WriteString("]\n")
1673 }
1674 buf.WriteString("---\n\n")
1675 return buf.String()
1676}
1677
1678func exportBoxSYZip(boxID string) (zipPath string) {
1679 util.PushEndlessProgress(Conf.Language(65))
1680 defer util.ClearPushProgress(100)
1681
1682 box := Conf.Box(boxID)
1683 if nil == box {
1684 logging.LogErrorf("not found box [%s]", boxID)
1685 return
1686 }
1687 baseFolderName := box.Name
1688
1689 var docPaths []string
1690 docFiles := box.ListFiles("/")
1691 for _, docFile := range docFiles {
1692 docPaths = append(docPaths, docFile.path)
1693 }
1694 zipPath = exportSYZip(boxID, "/", baseFolderName, docPaths)
1695 return
1696}
1697
1698func exportSYZip(boxID, rootDirPath, baseFolderName string, docPaths []string) (zipPath string) {
1699 defer util.ClearPushProgress(100)
1700
1701 dir, name := path.Split(baseFolderName)
1702 name = util.FilterFileName(name)
1703 if strings.HasSuffix(name, "..") {
1704 // 文档标题以 `..` 结尾时无法导出 Markdown https://github.com/siyuan-note/siyuan/issues/4698
1705 // 似乎是 os.MkdirAll 的 bug,以 .. 结尾的路径无法创建,所以这里加上 _ 结尾
1706 name += "_"
1707 }
1708 baseFolderName = path.Join(dir, name)
1709 box := Conf.Box(boxID)
1710
1711 exportDir := filepath.Join(util.TempDir, "export", baseFolderName)
1712 if err := os.MkdirAll(exportDir, 0755); err != nil {
1713 logging.LogErrorf("create export temp folder failed: %s", err)
1714 return
1715 }
1716
1717 trees := map[string]*parse.Tree{}
1718 refTrees := map[string]*parse.Tree{}
1719 luteEngine := util.NewLute()
1720 for i, p := range docPaths {
1721 if !strings.HasSuffix(p, ".sy") {
1722 continue
1723 }
1724
1725 tree, err := filesys.LoadTree(boxID, p, luteEngine)
1726 if err != nil {
1727 continue
1728 }
1729 trees[tree.ID] = tree
1730
1731 util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(70), fmt.Sprintf("%d/%d %s", i+1, len(docPaths), tree.Root.IALAttr("title"))))
1732 }
1733
1734 count := 1
1735 treeCache := map[string]*parse.Tree{}
1736 for _, tree := range trees {
1737 util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(70), fmt.Sprintf("%d/%d %s", count, len(docPaths), tree.Root.IALAttr("title"))))
1738
1739 refs := map[string]*parse.Tree{}
1740 exportRefTrees(tree, &[]string{}, refs, treeCache)
1741 for refTreeID, refTree := range refs {
1742 if nil == trees[refTreeID] {
1743 refTrees[refTreeID] = refTree
1744 }
1745 }
1746 count++
1747 }
1748
1749 util.PushEndlessProgress(Conf.Language(65))
1750 count = 0
1751
1752 // 按文件夹结构复制选择的树
1753 total := len(trees) + len(refTrees)
1754 for _, tree := range trees {
1755 readPath := filepath.Join(util.DataDir, tree.Box, tree.Path)
1756 data, readErr := filelock.ReadFile(readPath)
1757 if nil != readErr {
1758 logging.LogErrorf("read file [%s] failed: %s", readPath, readErr)
1759 continue
1760 }
1761
1762 writePath := strings.TrimPrefix(tree.Path, rootDirPath)
1763 writePath = filepath.Join(exportDir, writePath)
1764 writeFolder := filepath.Dir(writePath)
1765 if mkdirErr := os.MkdirAll(writeFolder, 0755); nil != mkdirErr {
1766 logging.LogErrorf("create export temp folder [%s] failed: %s", writeFolder, mkdirErr)
1767 continue
1768 }
1769 if writeErr := os.WriteFile(writePath, data, 0644); nil != writeErr {
1770 logging.LogErrorf("write export file [%s] failed: %s", writePath, writeErr)
1771 continue
1772 }
1773 count++
1774
1775 util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.Language(66), fmt.Sprintf("%d/%d ", count, total)+tree.HPath))
1776 }
1777
1778 count = 0
1779 // 引用树放在导出文件夹根路径下
1780 for treeID, tree := range refTrees {
1781 readPath := filepath.Join(util.DataDir, tree.Box, tree.Path)
1782 data, readErr := filelock.ReadFile(readPath)
1783 if nil != readErr {
1784 logging.LogErrorf("read file [%s] failed: %s", readPath, readErr)
1785 continue
1786 }
1787
1788 writePath := strings.TrimPrefix(tree.Path, rootDirPath)
1789 writePath = filepath.Join(exportDir, treeID+".sy")
1790 if writeErr := os.WriteFile(writePath, data, 0644); nil != writeErr {
1791 logging.LogErrorf("write export file [%s] failed: %s", writePath, writeErr)
1792 continue
1793 }
1794 count++
1795
1796 util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.Language(66), fmt.Sprintf("%d/%d ", count, total)+tree.HPath))
1797 }
1798
1799 // 将引用树合并到选择树中,以便后面一次性导出资源文件
1800 for treeID, tree := range refTrees {
1801 trees[treeID] = tree
1802 }
1803
1804 // 导出引用的资源文件
1805 assetPathMap, err := allAssetAbsPaths()
1806 if nil != err {
1807 logging.LogWarnf("get assets abs path failed: %s", err)
1808 return
1809 }
1810 copiedAssets := hashset.New()
1811 for _, tree := range trees {
1812 var assets []string
1813 assets = append(assets, getAssetsLinkDests(tree.Root)...)
1814 titleImgPath := treenode.GetDocTitleImgPath(tree.Root) // Export .sy.zip doc title image is not exported https://github.com/siyuan-note/siyuan/issues/8748
1815 if "" != titleImgPath {
1816 if util.IsAssetLinkDest([]byte(titleImgPath)) {
1817 assets = append(assets, titleImgPath)
1818 }
1819 }
1820
1821 for _, asset := range assets {
1822 util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(70), asset))
1823
1824 asset = string(html.DecodeDestination([]byte(asset)))
1825 if strings.Contains(asset, "?") {
1826 asset = asset[:strings.LastIndex(asset, "?")]
1827 }
1828
1829 if copiedAssets.Contains(asset) {
1830 continue
1831 }
1832
1833 srcPath := assetPathMap[asset]
1834 if "" == srcPath {
1835 logging.LogWarnf("get asset [%s] abs path failed", asset)
1836 continue
1837 }
1838
1839 destPath := filepath.Join(exportDir, asset)
1840 assetErr := filelock.Copy(srcPath, destPath)
1841 if nil != assetErr {
1842 logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", srcPath, destPath, assetErr)
1843 continue
1844 }
1845
1846 if !gulu.File.IsDir(srcPath) && strings.HasSuffix(strings.ToLower(srcPath), ".pdf") {
1847 sya := srcPath + ".sya"
1848 if filelock.IsExist(sya) {
1849 // Related PDF annotation information is not exported when exporting .sy.zip https://github.com/siyuan-note/siyuan/issues/7836
1850 if syaErr := filelock.Copy(sya, destPath+".sya"); nil != syaErr {
1851 logging.LogErrorf("copy sya from [%s] to [%s] failed: %s", sya, destPath+".sya", syaErr)
1852 }
1853 }
1854 }
1855
1856 copiedAssets.Add(asset)
1857 }
1858
1859 // 复制自定义表情图片
1860 emojis := emojisInTree(tree)
1861 for _, emoji := range emojis {
1862 from := filepath.Join(util.DataDir, emoji)
1863 to := filepath.Join(exportDir, emoji)
1864 if copyErr := filelock.Copy(from, to); copyErr != nil {
1865 logging.LogErrorf("copy emojis from [%s] to [%s] failed: %s", from, to, copyErr)
1866 }
1867 }
1868 }
1869
1870 // 导出数据库 Attribute View export https://github.com/siyuan-note/siyuan/issues/8710
1871 exportStorageAvDir := filepath.Join(exportDir, "storage", "av")
1872 var avIDs []string
1873 for _, tree := range trees {
1874 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
1875 if !entering || !n.IsBlock() {
1876 return ast.WalkContinue
1877 }
1878
1879 if ast.NodeAttributeView == n.Type {
1880 avIDs = append(avIDs, n.AttributeViewID)
1881 }
1882 avs := n.IALAttr(av.NodeAttrNameAvs)
1883 for _, avID := range strings.Split(avs, ",") {
1884 avIDs = append(avIDs, strings.TrimSpace(avID))
1885 }
1886 return ast.WalkContinue
1887 })
1888 }
1889 avIDs = gulu.Str.RemoveDuplicatedElem(avIDs)
1890 for _, avID := range avIDs {
1891 exportAv(avID, exportStorageAvDir, exportDir, assetPathMap)
1892 }
1893
1894 // 导出闪卡 Export related flashcard data when exporting .sy.zip https://github.com/siyuan-note/siyuan/issues/9372
1895 exportStorageRiffDir := filepath.Join(exportDir, "storage", "riff")
1896 deck, loadErr := riff.LoadDeck(exportStorageRiffDir, builtinDeckID, Conf.Flashcard.RequestRetention, Conf.Flashcard.MaximumInterval, Conf.Flashcard.Weights)
1897 if nil != loadErr {
1898 logging.LogErrorf("load deck [%s] failed: %s", name, loadErr)
1899 } else {
1900 for _, tree := range trees {
1901 cards := getTreeFlashcards(tree.ID)
1902
1903 for _, card := range cards {
1904 deck.AddCard(card.ID(), card.BlockID())
1905 }
1906 }
1907 if 0 < deck.CountCards() {
1908 if saveErr := deck.Save(); nil != saveErr {
1909 logging.LogErrorf("save deck [%s] failed: %s", name, saveErr)
1910 }
1911 }
1912 }
1913
1914 // 导出自定义排序
1915 sortPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json")
1916 fullSortIDs := map[string]int{}
1917 sortIDs := map[string]int{}
1918 var sortData []byte
1919 var sortErr error
1920 if filelock.IsExist(sortPath) {
1921 sortData, sortErr = filelock.ReadFile(sortPath)
1922 if nil != sortErr {
1923 logging.LogErrorf("read sort conf failed: %s", sortErr)
1924 }
1925
1926 if sortErr = gulu.JSON.UnmarshalJSON(sortData, &fullSortIDs); nil != sortErr {
1927 logging.LogErrorf("unmarshal sort conf failed: %s", sortErr)
1928 }
1929
1930 if 0 < len(fullSortIDs) {
1931 for _, tree := range trees {
1932 if v, ok := fullSortIDs[tree.ID]; ok {
1933 sortIDs[tree.ID] = v
1934 }
1935 }
1936 }
1937
1938 if 0 < len(sortIDs) {
1939 sortData, sortErr = gulu.JSON.MarshalJSON(sortIDs)
1940 if nil != sortErr {
1941 logging.LogErrorf("marshal sort conf failed: %s", sortErr)
1942 }
1943 if 0 < len(sortData) {
1944 confDir := filepath.Join(exportDir, ".siyuan")
1945 if mkdirErr := os.MkdirAll(confDir, 0755); nil != mkdirErr {
1946 logging.LogErrorf("create export conf folder [%s] failed: %s", confDir, mkdirErr)
1947 } else {
1948 sortPath = filepath.Join(confDir, "sort.json")
1949 if writeErr := os.WriteFile(sortPath, sortData, 0644); nil != writeErr {
1950 logging.LogErrorf("write sort conf failed: %s", writeErr)
1951 }
1952 }
1953 }
1954 }
1955 }
1956
1957 zipPath = exportDir + ".sy.zip"
1958 zip, err := gulu.Zip.Create(zipPath)
1959 if err != nil {
1960 logging.LogErrorf("create export .sy.zip [%s] failed: %s", exportDir, err)
1961 return ""
1962 }
1963
1964 zipCallback := func(filename string) {
1965 util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(253), filename))
1966 }
1967
1968 if err = zip.AddDirectory(baseFolderName, exportDir, zipCallback); err != nil {
1969 logging.LogErrorf("create export .sy.zip [%s] failed: %s", exportDir, err)
1970 return ""
1971 }
1972
1973 if err = zip.Close(); err != nil {
1974 logging.LogErrorf("close export .sy.zip failed: %s", err)
1975 }
1976
1977 os.RemoveAll(exportDir)
1978 zipPath = "/export/" + url.PathEscape(filepath.Base(zipPath))
1979 return
1980}
1981
1982func exportAv(avID, exportStorageAvDir, exportFolder string, assetPathMap map[string]string) {
1983 avJSONPath := av.GetAttributeViewDataPath(avID)
1984 if !filelock.IsExist(avJSONPath) {
1985 return
1986 }
1987
1988 if copyErr := filelock.Copy(avJSONPath, filepath.Join(exportStorageAvDir, avID+".json")); nil != copyErr {
1989 logging.LogErrorf("copy av json failed: %s", copyErr)
1990 }
1991
1992 attrView, err := av.ParseAttributeView(avID)
1993 if err != nil {
1994 logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
1995 return
1996 }
1997
1998 for _, keyValues := range attrView.KeyValues {
1999 switch keyValues.Key.Type {
2000 case av.KeyTypeMAsset: // 导出资源文件列 https://github.com/siyuan-note/siyuan/issues/9919
2001 for _, value := range keyValues.Values {
2002 for _, asset := range value.MAsset {
2003 if !util.IsAssetLinkDest([]byte(asset.Content)) {
2004 continue
2005 }
2006
2007 destPath := filepath.Join(exportFolder, asset.Content)
2008 srcPath := assetPathMap[asset.Content]
2009 if "" == srcPath {
2010 logging.LogWarnf("get asset [%s] abs path failed", asset.Content)
2011 continue
2012 }
2013
2014 if copyErr := filelock.Copy(srcPath, destPath); nil != copyErr {
2015 logging.LogErrorf("copy asset failed: %s", copyErr)
2016 }
2017 }
2018 }
2019 }
2020 }
2021
2022 // 级联导出关联列关联的数据库
2023 exportRelationAvs(avID, exportStorageAvDir)
2024}
2025
2026func exportRelationAvs(avID, exportStorageAvDir string) {
2027 avIDs := hashset.New()
2028 walkRelationAvs(avID, avIDs)
2029
2030 for _, v := range avIDs.Values() {
2031 relAvID := v.(string)
2032 relAvJSONPath := av.GetAttributeViewDataPath(relAvID)
2033 if !filelock.IsExist(relAvJSONPath) {
2034 continue
2035 }
2036
2037 if copyErr := filelock.Copy(relAvJSONPath, filepath.Join(exportStorageAvDir, relAvID+".json")); nil != copyErr {
2038 logging.LogErrorf("copy av json failed: %s", copyErr)
2039 }
2040 }
2041}
2042
2043func walkRelationAvs(avID string, exportAvIDs *hashset.Set) {
2044 if exportAvIDs.Contains(avID) {
2045 return
2046 }
2047
2048 attrView, _ := av.ParseAttributeView(avID)
2049 if nil == attrView {
2050 return
2051 }
2052
2053 exportAvIDs.Add(avID)
2054 for _, keyValues := range attrView.KeyValues {
2055 switch keyValues.Key.Type {
2056 case av.KeyTypeRelation: // 导出关联列
2057 if nil == keyValues.Key.Relation {
2058 break
2059 }
2060
2061 walkRelationAvs(keyValues.Key.Relation.AvID, exportAvIDs)
2062 }
2063 }
2064}
2065
2066func ExportMarkdownContent(id string, refMode, embedMode int, addYfm, fillCSSVar, adjustHeadingLv, imgTag bool) (hPath, exportedMd string) {
2067 bt := treenode.GetBlockTree(id)
2068 if nil == bt {
2069 return
2070 }
2071
2072 tree := prepareExportTree(bt)
2073 hPath = tree.HPath
2074 exportedMd = exportMarkdownContent0(id, tree, "", false, adjustHeadingLv, imgTag,
2075 ".md", refMode, embedMode, Conf.Export.FileAnnotationRefMode,
2076 Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker,
2077 Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight,
2078 Conf.Export.AddTitle, Conf.Export.InlineMemo, nil, true, fillCSSVar, map[string]*parse.Tree{})
2079 docIAL := parse.IAL2Map(tree.Root.KramdownIAL)
2080 if addYfm {
2081 exportedMd = yfm(docIAL) + exportedMd
2082 }
2083 return
2084}
2085
2086func exportMarkdownContent(id, ext string, exportRefMode int, defBlockIDs []string, singleFile bool, treeCache map[string]*parse.Tree) (tree *parse.Tree, exportedMd string, isEmpty bool) {
2087 tree, err := loadTreeWithCache(id, treeCache)
2088 if err != nil {
2089 logging.LogErrorf("load tree by block id [%s] failed: %s", id, err)
2090 return
2091 }
2092
2093 refCount := sql.QueryRootChildrenRefCount(tree.ID)
2094 if !Conf.Export.MarkdownYFM && treenode.ContainOnlyDefaultIAL(tree) && 1 > len(refCount) {
2095 for c := tree.Root.FirstChild; nil != c; c = c.Next {
2096 if ast.NodeParagraph == c.Type {
2097 isEmpty = nil == c.FirstChild
2098 if !isEmpty {
2099 break
2100 }
2101 } else {
2102 isEmpty = false
2103 break
2104 }
2105 }
2106 }
2107
2108 exportedMd = exportMarkdownContent0(id, tree, "", false, false, false,
2109 ext, exportRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode,
2110 Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker,
2111 Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight,
2112 Conf.Export.AddTitle, Conf.Export.InlineMemo, defBlockIDs, singleFile, false, treeCache)
2113 docIAL := parse.IAL2Map(tree.Root.KramdownIAL)
2114 if Conf.Export.MarkdownYFM {
2115 // 导出 Markdown 时在文档头添加 YFM 开关 https://github.com/siyuan-note/siyuan/issues/7727
2116 exportedMd = yfm(docIAL) + exportedMd
2117 }
2118 return
2119}
2120
2121func exportMarkdownContent0(id string, tree *parse.Tree, cloudAssetsBase string, assetsDestSpace2Underscore, adjustHeadingLv, imgTag bool,
2122 ext string, blockRefMode, blockEmbedMode, fileAnnotationRefMode int,
2123 tagOpenMarker, tagCloseMarker string, blockRefTextLeft, blockRefTextRight string,
2124 addTitle, inlineMemo bool, defBlockIDs []string, singleFile, fillCSSVar bool, treeCache map[string]*parse.Tree) (ret string) {
2125 tree = exportTree(tree, false, false, false,
2126 blockRefMode, blockEmbedMode, fileAnnotationRefMode,
2127 tagOpenMarker, tagCloseMarker,
2128 blockRefTextLeft, blockRefTextRight,
2129 addTitle, inlineMemo, 0 < len(defBlockIDs), singleFile, treeCache)
2130 if adjustHeadingLv {
2131 bt := treenode.GetBlockTree(id)
2132 adjustHeadingLevel(bt, tree)
2133 }
2134
2135 luteEngine := NewLute()
2136 luteEngine.SetFootnotes(true)
2137 luteEngine.SetKramdownIAL(false)
2138 if "" != cloudAssetsBase {
2139 luteEngine.RenderOptions.LinkBase = cloudAssetsBase
2140 }
2141 if assetsDestSpace2Underscore { // 上传到社区图床的资源文件会将空格转为下划线,所以这里也需要将文档内容做相应的转换
2142 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
2143 if !entering {
2144 return ast.WalkContinue
2145 }
2146
2147 if ast.NodeLinkDest == n.Type {
2148 if util.IsAssetLinkDest(n.Tokens) {
2149 n.Tokens = bytes.ReplaceAll(n.Tokens, []byte(" "), []byte("_"))
2150 }
2151 } else if n.IsTextMarkType("a") {
2152 href := n.TextMarkAHref
2153 if util.IsAssetLinkDest([]byte(href)) {
2154 n.TextMarkAHref = strings.ReplaceAll(href, " ", "_")
2155 }
2156 } else if ast.NodeIFrame == n.Type || ast.NodeAudio == n.Type || ast.NodeVideo == n.Type {
2157 dest := treenode.GetNodeSrcTokens(n)
2158 if util.IsAssetLinkDest([]byte(dest)) {
2159 setAssetsLinkDest(n, dest, strings.ReplaceAll(dest, " ", "_"))
2160 }
2161 }
2162 return ast.WalkContinue
2163 })
2164 }
2165
2166 currentDocDir := path.Dir(tree.HPath)
2167 currentDocDir = util.FilterFilePath(currentDocDir)
2168
2169 var unlinks []*ast.Node
2170 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
2171 if !entering {
2172 return ast.WalkContinue
2173 }
2174
2175 if ast.NodeBr == n.Type {
2176 if !n.ParentIs(ast.NodeTableCell) {
2177 // When exporting Markdown, `<br />` nodes in non-tables are replaced with `\n` text nodes https://github.com/siyuan-note/siyuan/issues/9509
2178 n.InsertBefore(&ast.Node{Type: ast.NodeText, Tokens: []byte("\n")})
2179 unlinks = append(unlinks, n)
2180 }
2181 }
2182
2183 if 4 == blockRefMode { // 脚注+锚点哈希
2184 if n.IsBlock() && gulu.Str.Contains(n.ID, defBlockIDs) {
2185 // 如果是定义块,则在开头处添加锚点
2186 anchorSpan := treenode.NewSpanAnchor(n.ID)
2187 if ast.NodeDocument != n.Type {
2188 firstLeaf := treenode.FirstLeafBlock(n)
2189 if nil != firstLeaf {
2190 if ast.NodeTable == firstLeaf.Type {
2191 firstLeaf.InsertBefore(anchorSpan)
2192 firstLeaf.InsertBefore(&ast.Node{Type: ast.NodeHardBreak})
2193 } else {
2194 if nil != firstLeaf.FirstChild {
2195 firstLeaf.FirstChild.InsertBefore(anchorSpan)
2196 } else {
2197 firstLeaf.AppendChild(anchorSpan)
2198 }
2199 }
2200 } else {
2201 n.AppendChild(anchorSpan)
2202 }
2203 }
2204 }
2205
2206 if treenode.IsBlockRef(n) {
2207 // 如果是引用元素,则将其转换为超链接,指向 xxx.md#block-id
2208 defID, linkText := getExportBlockRefLinkText(n, blockRefTextLeft, blockRefTextRight)
2209 if gulu.Str.Contains(defID, defBlockIDs) {
2210 var href string
2211 bt := treenode.GetBlockTree(defID)
2212 if nil != bt {
2213 href += bt.HPath + ext
2214 if "d" != bt.Type {
2215 href += "#" + defID
2216 }
2217 if tree.ID == bt.RootID {
2218 href = "#" + defID
2219 }
2220 }
2221
2222 sameDir := path.Dir(href) == currentDocDir
2223 href = util.FilterFilePath(href)
2224 if !sameDir {
2225 var relErr error
2226 href, relErr = filepath.Rel(currentDocDir, href)
2227 if nil != relErr {
2228 logging.LogWarnf("get relative path from [%s] to [%s] failed: %s", currentDocDir, href, relErr)
2229 }
2230 href = filepath.ToSlash(href)
2231 } else {
2232 href = strings.TrimPrefix(href, currentDocDir+"/")
2233 }
2234 blockRefLink := &ast.Node{Type: ast.NodeTextMark, TextMarkType: "a", TextMarkTextContent: linkText, TextMarkAHref: href}
2235 blockRefLink.KramdownIAL = n.KramdownIAL
2236 n.InsertBefore(blockRefLink)
2237 unlinks = append(unlinks, n)
2238 }
2239 }
2240 }
2241 return ast.WalkContinue
2242 })
2243 for _, unlink := range unlinks {
2244 unlink.Unlink()
2245 }
2246
2247 if fillCSSVar {
2248 fillThemeStyleVar(tree)
2249 }
2250
2251 luteEngine.SetUnorderedListMarker("-")
2252 luteEngine.SetImgTag(imgTag)
2253 renderer := render.NewProtyleExportMdRenderer(tree, luteEngine.RenderOptions)
2254 ret = gulu.Str.FromBytes(renderer.Render())
2255 return
2256}
2257
2258func exportTree(tree *parse.Tree, wysiwyg, keepFold, avHiddenCol bool,
2259 blockRefMode, blockEmbedMode, fileAnnotationRefMode int,
2260 tagOpenMarker, tagCloseMarker string,
2261 blockRefTextLeft, blockRefTextRight string,
2262 addTitle, inlineMemo, addDocAnchorSpan, singleFile bool, treeCache map[string]*parse.Tree) (ret *parse.Tree) {
2263 luteEngine := NewLute()
2264 ret = tree
2265 id := tree.Root.ID
2266 treeCache[tree.ID] = tree
2267
2268 // 解析查询嵌入节点
2269 depth := 0
2270 resolveEmbedR(ret.Root, blockEmbedMode, luteEngine, &[]string{}, &depth)
2271
2272 // 将块超链接转换为引用
2273 depth = 0
2274 blockLink2Ref(ret, ret.ID, treeCache, &depth)
2275
2276 // 收集引用转脚注+锚点哈希
2277 var refFootnotes []*refAsFootnotes
2278 if 4 == blockRefMode && singleFile {
2279 depth = 0
2280 collectFootnotesDefs(ret, ret.ID, &refFootnotes, treeCache, &depth)
2281 }
2282
2283 currentTreeNodeIDs := map[string]bool{}
2284 ast.Walk(ret.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
2285 if !entering {
2286 return ast.WalkContinue
2287 }
2288
2289 if "" != n.ID {
2290 currentTreeNodeIDs[n.ID] = true
2291 }
2292 return ast.WalkContinue
2293 })
2294
2295 var unlinks []*ast.Node
2296 ast.Walk(ret.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
2297 if !entering {
2298 return ast.WalkContinue
2299 }
2300
2301 switch n.Type {
2302 case ast.NodeSuperBlockOpenMarker, ast.NodeSuperBlockLayoutMarker, ast.NodeSuperBlockCloseMarker:
2303 if !wysiwyg {
2304 unlinks = append(unlinks, n)
2305 return ast.WalkContinue
2306 }
2307 case ast.NodeHeading:
2308 n.SetIALAttr("id", n.ID)
2309 case ast.NodeMathBlockContent:
2310 n.Tokens = bytes.TrimSpace(n.Tokens) // 导出 Markdown 时去除公式内容中的首尾空格 https://github.com/siyuan-note/siyuan/issues/4666
2311 return ast.WalkContinue
2312 case ast.NodeTextMark:
2313 if n.IsTextMarkType("inline-memo") {
2314 if !inlineMemo {
2315 n.TextMarkInlineMemoContent = ""
2316 }
2317 }
2318
2319 if n.IsTextMarkType("inline-math") {
2320 n.TextMarkInlineMathContent = strings.TrimSpace(n.TextMarkInlineMathContent)
2321 return ast.WalkContinue
2322 } else if treenode.IsFileAnnotationRef(n) {
2323 refID := n.TextMarkFileAnnotationRefID
2324 if !strings.Contains(refID, "/") {
2325 return ast.WalkSkipChildren
2326 }
2327
2328 status := processFileAnnotationRef(refID, n, fileAnnotationRefMode)
2329 unlinks = append(unlinks, n)
2330 return status
2331 } else if n.IsTextMarkType("tag") {
2332 if !wysiwyg {
2333 n.Type = ast.NodeText
2334 n.Tokens = []byte(tagOpenMarker + n.TextMarkTextContent + tagCloseMarker)
2335 return ast.WalkContinue
2336 }
2337 }
2338 }
2339
2340 if !treenode.IsBlockRef(n) {
2341 return ast.WalkContinue
2342 }
2343
2344 // 处理引用节点
2345 defID, linkText := getExportBlockRefLinkText(n, blockRefTextLeft, blockRefTextRight)
2346
2347 switch blockRefMode {
2348 case 2: // 锚文本块链
2349 blockRefLink := &ast.Node{Type: ast.NodeTextMark, TextMarkTextContent: linkText, TextMarkAHref: "siyuan://blocks/" + defID}
2350 blockRefLink.KramdownIAL = n.KramdownIAL
2351 blockRefLink.TextMarkType = "a " + n.TextMarkType
2352 blockRefLink.TextMarkInlineMemoContent = n.TextMarkInlineMemoContent
2353 n.InsertBefore(blockRefLink)
2354 unlinks = append(unlinks, n)
2355 case 3: // 仅锚文本
2356 blockRefLink := &ast.Node{Type: ast.NodeTextMark, TextMarkType: strings.TrimSpace(strings.ReplaceAll(n.TextMarkType, "block-ref", "")), TextMarkTextContent: linkText}
2357 blockRefLink.KramdownIAL = n.KramdownIAL
2358 blockRefLink.TextMarkInlineMemoContent = n.TextMarkInlineMemoContent
2359 n.InsertBefore(blockRefLink)
2360 unlinks = append(unlinks, n)
2361 case 4: // 脚注+锚点哈希
2362 if currentTreeNodeIDs[defID] {
2363 // 当前文档内不转换脚注,直接使用锚点哈希 https://github.com/siyuan-note/siyuan/issues/13283
2364 n.TextMarkType = "a " + n.TextMarkType
2365 n.TextMarkTextContent = linkText
2366 n.TextMarkAHref = "#" + defID
2367 return ast.WalkContinue
2368 }
2369
2370 refFoot := getRefAsFootnotes(defID, &refFootnotes)
2371 if nil == refFoot {
2372 return ast.WalkContinue
2373 }
2374
2375 text := &ast.Node{Type: ast.NodeText, Tokens: []byte(linkText)}
2376 if "block-ref" != n.TextMarkType {
2377 text.Type = ast.NodeTextMark
2378 text.TextMarkType = strings.TrimSpace(strings.ReplaceAll(n.TextMarkType, "block-ref", ""))
2379 text.TextMarkTextContent = linkText
2380 text.TextMarkInlineMemoContent = n.TextMarkInlineMemoContent
2381 }
2382 n.InsertBefore(text)
2383 n.InsertBefore(&ast.Node{Type: ast.NodeFootnotesRef, Tokens: []byte("^" + refFoot.refNum), FootnotesRefId: refFoot.refNum, FootnotesRefLabel: []byte("^" + refFoot.refNum)})
2384 unlinks = append(unlinks, n)
2385 }
2386 return ast.WalkSkipChildren
2387 })
2388 for _, n := range unlinks {
2389 n.Unlink()
2390 }
2391
2392 if 4 == blockRefMode { // 脚注+锚点哈希
2393 unlinks = nil
2394 footnotesDefBlock := resolveFootnotesDefs(&refFootnotes, ret, currentTreeNodeIDs, blockRefTextLeft, blockRefTextRight, treeCache)
2395 if nil != footnotesDefBlock {
2396 // 如果是聚焦导出,可能存在没有使用的脚注定义块,在这里进行清理
2397 // Improve focus export conversion of block refs to footnotes https://github.com/siyuan-note/siyuan/issues/10647
2398 footnotesRefs := ret.Root.ChildrenByType(ast.NodeFootnotesRef)
2399 for footnotesDef := footnotesDefBlock.FirstChild; nil != footnotesDef; footnotesDef = footnotesDef.Next {
2400 fnRefsInDef := footnotesDef.ChildrenByType(ast.NodeFootnotesRef)
2401 footnotesRefs = append(footnotesRefs, fnRefsInDef...)
2402 }
2403
2404 for footnotesDef := footnotesDefBlock.FirstChild; nil != footnotesDef; footnotesDef = footnotesDef.Next {
2405 exist := false
2406 for _, ref := range footnotesRefs {
2407 if ref.FootnotesRefId == footnotesDef.FootnotesRefId {
2408 exist = true
2409 break
2410 }
2411 }
2412 if !exist {
2413 unlinks = append(unlinks, footnotesDef)
2414 }
2415 }
2416
2417 for _, n := range unlinks {
2418 n.Unlink()
2419 }
2420
2421 ret.Root.AppendChild(footnotesDefBlock)
2422 }
2423 }
2424
2425 if addTitle {
2426 if root, _ := getBlock(id, tree); nil != root {
2427 root.IAL["type"] = "doc"
2428 title := &ast.Node{Type: ast.NodeHeading, HeadingLevel: 1}
2429 for k, v := range root.IAL {
2430 if "type" == k {
2431 continue
2432 }
2433 title.SetIALAttr(k, v)
2434 }
2435 title.InsertAfter(&ast.Node{Type: ast.NodeKramdownBlockIAL, Tokens: parse.IAL2Tokens(title.KramdownIAL)})
2436 content := html.UnescapeString(root.Content)
2437 title.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(content)})
2438 ret.Root.PrependChild(title)
2439 }
2440 } else {
2441 if 4 == blockRefMode { // 脚注+锚点哈希
2442 refRoot := false
2443
2444 for _, refFoot := range refFootnotes {
2445 if id == refFoot.defID {
2446 refRoot = true
2447 break
2448 }
2449 }
2450
2451 footnotesDefs := tree.Root.ChildrenByType(ast.NodeFootnotesDef)
2452 for _, footnotesDef := range footnotesDefs {
2453 ast.Walk(footnotesDef, func(n *ast.Node, entering bool) ast.WalkStatus {
2454 if !entering {
2455 return ast.WalkContinue
2456 }
2457
2458 if id == n.TextMarkBlockRefID {
2459 refRoot = true
2460 return ast.WalkStop
2461 }
2462 return ast.WalkContinue
2463 })
2464 }
2465
2466 if refRoot && addDocAnchorSpan {
2467 anchorSpan := treenode.NewSpanAnchor(id)
2468 ret.Root.PrependChild(anchorSpan)
2469 }
2470 }
2471 }
2472
2473 // 导出时支持导出题头图 https://github.com/siyuan-note/siyuan/issues/4372
2474 titleImgPath := treenode.GetDocTitleImgPath(ret.Root)
2475 if "" != titleImgPath {
2476 p := &ast.Node{Type: ast.NodeParagraph}
2477 titleImg := &ast.Node{Type: ast.NodeImage}
2478 titleImg.AppendChild(&ast.Node{Type: ast.NodeBang})
2479 titleImg.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
2480 titleImg.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte("image")})
2481 titleImg.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
2482 titleImg.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
2483 titleImg.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(titleImgPath)})
2484 titleImg.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
2485 p.AppendChild(titleImg)
2486 ret.Root.PrependChild(p)
2487 }
2488
2489 unlinks = nil
2490 var emptyParagraphs []*ast.Node
2491 ast.Walk(ret.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
2492 if !entering {
2493 return ast.WalkContinue
2494 }
2495
2496 // 支持按照现有折叠状态导出 PDF https://github.com/siyuan-note/siyuan/issues/5941
2497 if !keepFold {
2498 // 块折叠以后导出 HTML/PDF 固定展开 https://github.com/siyuan-note/siyuan/issues/4064
2499 n.RemoveIALAttr("fold")
2500 n.RemoveIALAttr("heading-fold")
2501 } else {
2502 if "1" == n.IALAttr("heading-fold") {
2503 unlinks = append(unlinks, n)
2504 return ast.WalkContinue
2505 }
2506 }
2507
2508 // 导出时去掉内容块闪卡样式 https://github.com/siyuan-note/siyuan/issues/7374
2509 if n.IsBlock() {
2510 n.RemoveIALAttr("custom-riff-decks")
2511 }
2512
2513 switch n.Type {
2514 case ast.NodeParagraph:
2515 if nil == n.FirstChild {
2516 // 空的段落块需要补全文本展位,否则后续格式化后再解析树会语义不一致 https://github.com/siyuan-note/siyuan/issues/5806
2517 emptyParagraphs = append(emptyParagraphs, n)
2518 }
2519 case ast.NodeWidget:
2520 // 挂件块导出 https://github.com/siyuan-note/siyuan/issues/3834 https://github.com/siyuan-note/siyuan/issues/6188
2521
2522 if wysiwyg {
2523 exportHtmlVal := n.IALAttr("data-export-html")
2524 if "" != exportHtmlVal {
2525 htmlBlock := &ast.Node{Type: ast.NodeHTMLBlock, Tokens: []byte(exportHtmlVal)}
2526 n.InsertBefore(htmlBlock)
2527 unlinks = append(unlinks, n)
2528 return ast.WalkContinue
2529 }
2530 }
2531
2532 exportMdVal := n.IALAttr("data-export-md")
2533 exportMdVal = html.UnescapeString(exportMdVal) // 导出 `data-export-md` 时未解析代码块与行内代码内的转义字符 https://github.com/siyuan-note/siyuan/issues/4180
2534 if "" != exportMdVal {
2535 luteEngine0 := util.NewLute()
2536 luteEngine0.SetYamlFrontMatter(true) // 挂件导出属性 `data-export-md` 支持 YFM https://github.com/siyuan-note/siyuan/issues/7752
2537 exportMdTree := parse.Parse("", []byte(exportMdVal), luteEngine0.ParseOptions)
2538 var insertNodes []*ast.Node
2539 for c := exportMdTree.Root.FirstChild; nil != c; c = c.Next {
2540 if ast.NodeKramdownBlockIAL != c.Type {
2541 insertNodes = append(insertNodes, c)
2542 }
2543 }
2544 for _, insertNode := range insertNodes {
2545 n.InsertBefore(insertNode)
2546 }
2547 unlinks = append(unlinks, n)
2548 }
2549 case ast.NodeSuperBlockOpenMarker, ast.NodeSuperBlockLayoutMarker, ast.NodeSuperBlockCloseMarker:
2550 if !wysiwyg {
2551 unlinks = append(unlinks, n)
2552 }
2553 }
2554
2555 if ast.NodeText != n.Type {
2556 return ast.WalkContinue
2557 }
2558
2559 // Shift+Enter 换行在导出为 Markdown 时使用硬换行 https://github.com/siyuan-note/siyuan/issues/3458
2560 n.Tokens = bytes.ReplaceAll(n.Tokens, []byte("\n"), []byte(" \n"))
2561 return ast.WalkContinue
2562 })
2563 for _, n := range unlinks {
2564 n.Unlink()
2565 }
2566 for _, emptyParagraph := range emptyParagraphs {
2567 emptyParagraph.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(editor.Zwj)})
2568 }
2569
2570 unlinks = nil
2571 // Attribute View export https://github.com/siyuan-note/siyuan/issues/8710
2572 ast.Walk(ret.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
2573 if !entering {
2574 return ast.WalkContinue
2575 }
2576
2577 if ast.NodeAttributeView != n.Type {
2578 return ast.WalkContinue
2579 }
2580
2581 avID := n.AttributeViewID
2582 if avJSONPath := av.GetAttributeViewDataPath(avID); !filelock.IsExist(avJSONPath) {
2583 return ast.WalkContinue
2584 }
2585
2586 attrView, err := av.ParseAttributeView(avID)
2587 if err != nil {
2588 logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err)
2589 return ast.WalkContinue
2590 }
2591
2592 viewID := n.IALAttr(av.NodeAttrView)
2593 view, err := attrView.GetCurrentView(viewID)
2594 if err != nil {
2595 logging.LogErrorf("get attribute view [%s] failed: %s", avID, err)
2596 return ast.WalkContinue
2597 }
2598
2599 table := getAttrViewTable(attrView, view, "")
2600
2601 // 遵循视图过滤和排序规则 Use filtering and sorting of current view settings when exporting database blocks https://github.com/siyuan-note/siyuan/issues/10474
2602 cachedAttrViews := map[string]*av.AttributeView{}
2603 rollupFurtherCollections := sql.GetFurtherCollections(attrView, cachedAttrViews)
2604 av.Filter(table, attrView, rollupFurtherCollections, cachedAttrViews)
2605 av.Sort(table, attrView)
2606
2607 var aligns []int
2608 for range table.Columns {
2609 aligns = append(aligns, 0)
2610 }
2611 mdTable := &ast.Node{Type: ast.NodeTable, TableAligns: aligns}
2612 mdTableHead := &ast.Node{Type: ast.NodeTableHead}
2613 mdTable.AppendChild(mdTableHead)
2614 mdTableHeadRow := &ast.Node{Type: ast.NodeTableRow, TableAligns: aligns}
2615 mdTableHead.AppendChild(mdTableHeadRow)
2616 for _, col := range table.Columns {
2617 if avHiddenCol && col.Hidden {
2618 // 按需跳过隐藏列 Improve database table view exporting https://github.com/siyuan-note/siyuan/issues/12232
2619 continue
2620 }
2621
2622 cell := &ast.Node{Type: ast.NodeTableCell}
2623 name := col.Name
2624 if !wysiwyg {
2625 name = string(lex.EscapeProtyleMarkers([]byte(col.Name)))
2626 name = strings.ReplaceAll(name, "\\|", "|")
2627 name = strings.ReplaceAll(name, "|", "\\|")
2628 }
2629 cell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(name)})
2630 mdTableHeadRow.AppendChild(cell)
2631 }
2632
2633 rowNum := 1
2634 for _, row := range table.Rows {
2635 mdTableRow := &ast.Node{Type: ast.NodeTableRow, TableAligns: aligns}
2636 mdTable.AppendChild(mdTableRow)
2637 for _, cell := range row.Cells {
2638 if avHiddenCol && nil != cell.Value {
2639 if col := table.GetColumn(cell.Value.KeyID); nil != col && col.Hidden {
2640 continue
2641 }
2642 }
2643
2644 mdTableCell := &ast.Node{Type: ast.NodeTableCell}
2645 mdTableRow.AppendChild(mdTableCell)
2646 var val string
2647 if nil != cell.Value {
2648 if av.KeyTypeBlock == cell.Value.Type {
2649 if nil != cell.Value.Block {
2650 val = cell.Value.Block.Content
2651 if !wysiwyg {
2652 val = string(lex.EscapeProtyleMarkers([]byte(val)))
2653 val = strings.ReplaceAll(val, "\\|", "|")
2654 val = strings.ReplaceAll(val, "|", "\\|")
2655 }
2656 col := table.GetColumn(cell.Value.KeyID)
2657 if nil != col && col.Wrap {
2658 lines := strings.Split(val, "\n")
2659 for _, line := range lines {
2660 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(line)})
2661 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeHardBreak})
2662 }
2663 } else {
2664 val = strings.ReplaceAll(val, "\n", " ")
2665 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(val)})
2666 }
2667 continue
2668 }
2669 } else if av.KeyTypeText == cell.Value.Type {
2670 if nil != cell.Value.Text {
2671 val = cell.Value.Text.Content
2672 if !wysiwyg {
2673 val = string(lex.EscapeProtyleMarkers([]byte(val)))
2674 val = strings.ReplaceAll(val, "\\|", "|")
2675 val = strings.ReplaceAll(val, "|", "\\|")
2676 }
2677 col := table.GetColumn(cell.Value.KeyID)
2678 if nil != col && col.Wrap {
2679 lines := strings.Split(val, "\n")
2680 for _, line := range lines {
2681 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(line)})
2682 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeHardBreak})
2683 }
2684 } else {
2685 val = strings.ReplaceAll(val, "\n", " ")
2686 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(val)})
2687 }
2688 continue
2689 }
2690 } else if av.KeyTypeTemplate == cell.Value.Type {
2691 if nil != cell.Value.Template {
2692 val = cell.Value.Template.Content
2693 val = strings.ReplaceAll(val, "\\|", "|")
2694 val = strings.ReplaceAll(val, "|", "\\|")
2695 col := table.GetColumn(cell.Value.KeyID)
2696 if nil != col && col.Wrap {
2697 lines := strings.Split(val, "\n")
2698 for _, line := range lines {
2699 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(line)})
2700 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeHardBreak})
2701 }
2702 } else {
2703 val = strings.ReplaceAll(val, "\n", " ")
2704 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(val)})
2705 }
2706 continue
2707 }
2708 } else if av.KeyTypeDate == cell.Value.Type {
2709 if nil != cell.Value.Date {
2710 cell.Value.Date = av.NewFormattedValueDate(cell.Value.Date.Content, cell.Value.Date.Content2, av.DateFormatNone, cell.Value.Date.IsNotTime, cell.Value.Date.HasEndDate)
2711 }
2712 } else if av.KeyTypeCreated == cell.Value.Type {
2713 if nil != cell.Value.Created {
2714 cell.Value.Created = av.NewFormattedValueCreated(cell.Value.Created.Content, 0, av.CreatedFormatNone)
2715 }
2716 } else if av.KeyTypeUpdated == cell.Value.Type {
2717 if nil != cell.Value.Updated {
2718 cell.Value.Updated = av.NewFormattedValueUpdated(cell.Value.Updated.Content, 0, av.UpdatedFormatNone)
2719 }
2720 } else if av.KeyTypeURL == cell.Value.Type {
2721 if nil != cell.Value.URL {
2722 if "" != strings.TrimSpace(cell.Value.URL.Content) {
2723 link := &ast.Node{Type: ast.NodeLink}
2724 link.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
2725 link.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte(cell.Value.URL.Content)})
2726 link.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
2727 link.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
2728 link.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(cell.Value.URL.Content)})
2729 link.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
2730 mdTableCell.AppendChild(link)
2731 }
2732 continue
2733 }
2734 } else if av.KeyTypeMAsset == cell.Value.Type {
2735 if nil != cell.Value.MAsset {
2736 for i, a := range cell.Value.MAsset {
2737 if av.AssetTypeImage == a.Type {
2738 img := &ast.Node{Type: ast.NodeImage}
2739 img.AppendChild(&ast.Node{Type: ast.NodeBang})
2740 img.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
2741 img.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte(a.Name)})
2742 img.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
2743 img.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
2744 img.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(a.Content)})
2745 img.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
2746 mdTableCell.AppendChild(img)
2747 } else if av.AssetTypeFile == a.Type {
2748 linkText := strings.TrimSpace(a.Name)
2749 if "" == linkText {
2750 linkText = a.Content
2751 }
2752
2753 if "" != strings.TrimSpace(a.Content) {
2754 file := &ast.Node{Type: ast.NodeLink}
2755 file.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
2756 file.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte(linkText)})
2757 file.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
2758 file.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
2759 file.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(a.Content)})
2760 file.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
2761 mdTableCell.AppendChild(file)
2762 } else {
2763 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(linkText)})
2764 }
2765 }
2766 if i < len(cell.Value.MAsset)-1 {
2767 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(" ")})
2768 }
2769 }
2770 continue
2771 }
2772 } else if av.KeyTypeLineNumber == cell.Value.Type {
2773 val = strconv.Itoa(rowNum)
2774 rowNum++
2775 } else if av.KeyTypeRelation == cell.Value.Type {
2776 for i, v := range cell.Value.Relation.Contents {
2777 if nil == v {
2778 continue
2779 }
2780
2781 if av.KeyTypeBlock == v.Type && nil != v.Block {
2782 val = v.Block.Content
2783 if !wysiwyg {
2784 val = string(lex.EscapeProtyleMarkers([]byte(val)))
2785 val = strings.ReplaceAll(val, "\\|", "|")
2786 val = strings.ReplaceAll(val, "|", "\\|")
2787 }
2788
2789 col := table.GetColumn(cell.Value.KeyID)
2790 if nil != col && col.Wrap {
2791 lines := strings.Split(val, "\n")
2792 for _, line := range lines {
2793 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(line)})
2794 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeHardBreak})
2795 }
2796 } else {
2797 val = strings.ReplaceAll(val, "\n", " ")
2798 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(val)})
2799 }
2800 }
2801 if i < len(cell.Value.Relation.Contents)-1 {
2802 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(", ")})
2803 }
2804 }
2805 continue
2806 } else if av.KeyTypeRollup == cell.Value.Type {
2807 for i, v := range cell.Value.Rollup.Contents {
2808 if nil == v {
2809 continue
2810 }
2811
2812 if av.KeyTypeBlock == v.Type {
2813 if nil != v.Block {
2814 val = v.Block.Content
2815 if !wysiwyg {
2816 val = string(lex.EscapeProtyleMarkers([]byte(val)))
2817 val = strings.ReplaceAll(val, "\\|", "|")
2818 val = strings.ReplaceAll(val, "|", "\\|")
2819 }
2820
2821 col := table.GetColumn(cell.Value.KeyID)
2822 if nil != col && col.Wrap {
2823 lines := strings.Split(val, "\n")
2824 for _, line := range lines {
2825 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(line)})
2826 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeHardBreak})
2827 }
2828 } else {
2829 val = strings.ReplaceAll(val, "\n", " ")
2830 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(val)})
2831 }
2832 }
2833 } else if av.KeyTypeText == v.Type {
2834 val = v.Text.Content
2835 if !wysiwyg {
2836 val = string(lex.EscapeProtyleMarkers([]byte(val)))
2837 val = strings.ReplaceAll(val, "\\|", "|")
2838 val = strings.ReplaceAll(val, "|", "\\|")
2839 }
2840
2841 col := table.GetColumn(cell.Value.KeyID)
2842 if nil != col && col.Wrap {
2843 lines := strings.Split(val, "\n")
2844 for _, line := range lines {
2845 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(line)})
2846 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeHardBreak})
2847 }
2848 } else {
2849 val = strings.ReplaceAll(val, "\n", " ")
2850 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(val)})
2851 }
2852 } else {
2853 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(v.String(true))})
2854 }
2855
2856 if i < len(cell.Value.Rollup.Contents)-1 {
2857 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(", ")})
2858 }
2859 }
2860 continue
2861 }
2862
2863 if "" == val {
2864 val = cell.Value.String(true)
2865 }
2866 }
2867 mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(val)})
2868 }
2869 }
2870
2871 n.InsertBefore(mdTable)
2872 unlinks = append(unlinks, n)
2873 return ast.WalkContinue
2874 })
2875 for _, n := range unlinks {
2876 n.Unlink()
2877 }
2878 return ret
2879}
2880
2881func resolveFootnotesDefs(refFootnotes *[]*refAsFootnotes, currentTree *parse.Tree, currentTreeNodeIDs map[string]bool, blockRefTextLeft, blockRefTextRight string, treeCache map[string]*parse.Tree) (footnotesDefBlock *ast.Node) {
2882 if 1 > len(*refFootnotes) {
2883 return nil
2884 }
2885
2886 footnotesDefBlock = &ast.Node{Type: ast.NodeFootnotesDefBlock}
2887 var rendered []string
2888 for _, foot := range *refFootnotes {
2889 t, err := loadTreeWithCache(foot.defID, treeCache)
2890 if nil != err {
2891 return
2892 }
2893
2894 defNode := treenode.GetNodeInTree(t, foot.defID)
2895 docID := util.GetTreeID(defNode.Path)
2896 var nodes []*ast.Node
2897 if ast.NodeHeading == defNode.Type {
2898 nodes = append(nodes, defNode)
2899 if currentTree.ID != docID {
2900 // 同文档块引转脚注缩略定义考虑容器块和标题块 https://github.com/siyuan-note/siyuan/issues/5917
2901 children := treenode.HeadingChildren(defNode)
2902 nodes = append(nodes, children...)
2903 }
2904 } else if ast.NodeDocument == defNode.Type {
2905 docTitle := &ast.Node{ID: defNode.ID, Type: ast.NodeHeading, HeadingLevel: 1}
2906 docTitle.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(defNode.IALAttr("title"))})
2907 nodes = append(nodes, docTitle)
2908 for c := defNode.FirstChild; nil != c; c = c.Next {
2909 nodes = append(nodes, c)
2910 }
2911 } else {
2912 nodes = append(nodes, defNode)
2913 }
2914
2915 var newNodes []*ast.Node
2916 for _, node := range nodes {
2917 var unlinks []*ast.Node
2918
2919 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
2920 if !entering {
2921 return ast.WalkContinue
2922 }
2923
2924 if treenode.IsBlockRef(n) {
2925 defID, _, _ := treenode.GetBlockRef(n)
2926 if f := getRefAsFootnotes(defID, refFootnotes); nil != f {
2927 n.InsertBefore(&ast.Node{Type: ast.NodeText, Tokens: []byte(blockRefTextLeft + f.refAnchorText + blockRefTextRight)})
2928 n.InsertBefore(&ast.Node{Type: ast.NodeFootnotesRef, Tokens: []byte("^" + f.refNum), FootnotesRefId: f.refNum, FootnotesRefLabel: []byte("^" + f.refNum)})
2929 unlinks = append(unlinks, n)
2930 } else {
2931 if isNodeInTree(defID, currentTree) {
2932 if currentTreeNodeIDs[defID] {
2933 // 当前文档内不转换脚注,直接使用锚点哈希 https://github.com/siyuan-note/siyuan/issues/13283
2934 n.TextMarkType = "a"
2935 n.TextMarkTextContent = blockRefTextLeft + n.TextMarkTextContent + blockRefTextRight
2936 n.TextMarkAHref = "#" + defID
2937 return ast.WalkSkipChildren
2938 }
2939 }
2940 }
2941 return ast.WalkSkipChildren
2942 } else if ast.NodeBlockQueryEmbed == n.Type {
2943 stmt := n.ChildByType(ast.NodeBlockQueryEmbedScript).TokensStr()
2944 stmt = html.UnescapeString(stmt)
2945 stmt = strings.ReplaceAll(stmt, editor.IALValEscNewLine, "\n")
2946 sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, Conf.Search.Limit)
2947 for _, b := range sqlBlocks {
2948 subNodes := renderBlockMarkdownR(b.ID, &rendered)
2949 for _, subNode := range subNodes {
2950 if ast.NodeListItem == subNode.Type {
2951 parentList := &ast.Node{Type: ast.NodeList, ListData: &ast.ListData{Typ: subNode.ListData.Typ}}
2952 parentList.AppendChild(subNode)
2953 newNodes = append(newNodes, parentList)
2954 } else {
2955 newNodes = append(newNodes, subNode)
2956 }
2957 }
2958 }
2959 unlinks = append(unlinks, n)
2960 return ast.WalkSkipChildren
2961 }
2962 return ast.WalkContinue
2963 })
2964 for _, n := range unlinks {
2965 n.Unlink()
2966 }
2967
2968 if ast.NodeBlockQueryEmbed != node.Type {
2969 if ast.NodeListItem == node.Type {
2970 parentList := &ast.Node{Type: ast.NodeList, ListData: &ast.ListData{Typ: node.ListData.Typ}}
2971 parentList.AppendChild(node)
2972 newNodes = append(newNodes, parentList)
2973 } else {
2974 newNodes = append(newNodes, node)
2975 }
2976 }
2977 }
2978
2979 footnotesDef := &ast.Node{Type: ast.NodeFootnotesDef, Tokens: []byte("^" + foot.refNum), FootnotesRefId: foot.refNum, FootnotesRefLabel: []byte("^" + foot.refNum)}
2980 for _, node := range newNodes {
2981 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
2982 if !entering {
2983 return ast.WalkContinue
2984 }
2985 if ast.NodeParagraph != n.Type {
2986 return ast.WalkContinue
2987 }
2988
2989 docID := util.GetTreeID(n.Path)
2990 if currentTree.ID == docID {
2991 // 同文档块引转脚注缩略定义 https://github.com/siyuan-note/siyuan/issues/3299
2992 if text := sql.GetRefText(n.ID); 64 < utf8.RuneCountInString(text) {
2993 var unlinkChildren []*ast.Node
2994 for c := n.FirstChild; nil != c; c = c.Next {
2995 unlinkChildren = append(unlinkChildren, c)
2996 }
2997 for _, c := range unlinkChildren {
2998 c.Unlink()
2999 }
3000 text = gulu.Str.SubStr(text, 64) + "..."
3001 n.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(text)})
3002 return ast.WalkSkipChildren
3003 }
3004 }
3005 return ast.WalkContinue
3006 })
3007
3008 footnotesDef.AppendChild(node)
3009 }
3010 footnotesDefBlock.AppendChild(footnotesDef)
3011 }
3012 return
3013}
3014
3015func blockLink2Ref(currentTree *parse.Tree, id string, treeCache map[string]*parse.Tree, depth *int) {
3016 *depth++
3017 if 4096 < *depth {
3018 return
3019 }
3020
3021 b := treenode.GetBlockTree(id)
3022 if nil == b {
3023 return
3024 }
3025 t, err := loadTreeWithCache(b.RootID, treeCache)
3026 if nil != err {
3027 return
3028 }
3029
3030 node := treenode.GetNodeInTree(t, b.ID)
3031 if nil == node {
3032 logging.LogErrorf("not found node [%s] in tree [%s]", b.ID, t.Root.ID)
3033 return
3034 }
3035 blockLink2Ref0(currentTree, node, treeCache, depth)
3036 if ast.NodeHeading == node.Type {
3037 children := treenode.HeadingChildren(node)
3038 for _, c := range children {
3039 blockLink2Ref0(currentTree, c, treeCache, depth)
3040 }
3041 }
3042 return
3043}
3044
3045func blockLink2Ref0(currentTree *parse.Tree, node *ast.Node, treeCache map[string]*parse.Tree, depth *int) {
3046 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
3047 if !entering {
3048 return ast.WalkContinue
3049 }
3050
3051 if treenode.IsBlockLink(n) {
3052 n.TextMarkType = strings.TrimSpace(strings.TrimPrefix(n.TextMarkType, "a") + " block-ref")
3053 n.TextMarkBlockRefID = strings.TrimPrefix(n.TextMarkAHref, "siyuan://blocks/")
3054 n.TextMarkBlockRefSubtype = "s"
3055
3056 blockLink2Ref(currentTree, n.TextMarkBlockRefID, treeCache, depth)
3057 return ast.WalkSkipChildren
3058 } else if treenode.IsBlockRef(n) {
3059 defID, _, _ := treenode.GetBlockRef(n)
3060 blockLink2Ref(currentTree, defID, treeCache, depth)
3061 }
3062 return ast.WalkContinue
3063 })
3064}
3065
3066func collectFootnotesDefs(currentTree *parse.Tree, id string, refFootnotes *[]*refAsFootnotes, treeCache map[string]*parse.Tree, depth *int) {
3067 *depth++
3068 if 4096 < *depth {
3069 return
3070 }
3071 b := treenode.GetBlockTree(id)
3072 if nil == b {
3073 return
3074 }
3075 t, err := loadTreeWithCache(b.RootID, treeCache)
3076 if nil != err {
3077 return
3078 }
3079
3080 node := treenode.GetNodeInTree(t, b.ID)
3081 if nil == node {
3082 logging.LogErrorf("not found node [%s] in tree [%s]", b.ID, t.Root.ID)
3083 return
3084 }
3085 collectFootnotesDefs0(currentTree, node, refFootnotes, treeCache, depth)
3086 if ast.NodeHeading == node.Type {
3087 children := treenode.HeadingChildren(node)
3088 for _, c := range children {
3089 collectFootnotesDefs0(currentTree, c, refFootnotes, treeCache, depth)
3090 }
3091 }
3092 return
3093}
3094
3095func collectFootnotesDefs0(currentTree *parse.Tree, node *ast.Node, refFootnotes *[]*refAsFootnotes, treeCache map[string]*parse.Tree, depth *int) {
3096 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
3097 if !entering {
3098 return ast.WalkContinue
3099 }
3100
3101 if treenode.IsBlockRef(n) {
3102 defID, refText, _ := treenode.GetBlockRef(n)
3103 if nil == getRefAsFootnotes(defID, refFootnotes) {
3104 if isNodeInTree(defID, currentTree) {
3105 // 当前文档内不转换脚注,直接使用锚点哈希 https://github.com/siyuan-note/siyuan/issues/13283
3106 return ast.WalkSkipChildren
3107 }
3108 anchorText := refText
3109 if Conf.Editor.BlockRefDynamicAnchorTextMaxLen < utf8.RuneCountInString(anchorText) {
3110 anchorText = gulu.Str.SubStr(anchorText, Conf.Editor.BlockRefDynamicAnchorTextMaxLen) + "..."
3111 }
3112 *refFootnotes = append(*refFootnotes, &refAsFootnotes{
3113 defID: defID,
3114 refNum: strconv.Itoa(len(*refFootnotes) + 1),
3115 refAnchorText: anchorText,
3116 })
3117 collectFootnotesDefs(currentTree, defID, refFootnotes, treeCache, depth)
3118 }
3119 return ast.WalkSkipChildren
3120 }
3121 return ast.WalkContinue
3122 })
3123}
3124
3125func isNodeInTree(id string, tree *parse.Tree) (ret bool) {
3126 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
3127 if !entering {
3128 return ast.WalkContinue
3129 }
3130
3131 if n.ID == id {
3132 ret = true
3133 return ast.WalkStop
3134 }
3135 return ast.WalkContinue
3136 })
3137 return
3138}
3139
3140func getRefAsFootnotes(defID string, slice *[]*refAsFootnotes) *refAsFootnotes {
3141 for _, e := range *slice {
3142 if e.defID == defID {
3143 return e
3144 }
3145 }
3146 return nil
3147}
3148
3149type refAsFootnotes struct {
3150 defID string
3151 refNum string
3152 refAnchorText string
3153}
3154
3155func processFileAnnotationRef(refID string, n *ast.Node, fileAnnotationRefMode int) ast.WalkStatus {
3156 p := refID[:strings.LastIndex(refID, "/")]
3157 absPath, err := GetAssetAbsPath(p)
3158 if err != nil {
3159 logging.LogWarnf("get assets abs path by rel path [%s] failed: %s", p, err)
3160 return ast.WalkSkipChildren
3161 }
3162 sya := absPath + ".sya"
3163 syaData, err := os.ReadFile(sya)
3164 if err != nil {
3165 logging.LogErrorf("read file [%s] failed: %s", sya, err)
3166 return ast.WalkSkipChildren
3167 }
3168 syaJSON := map[string]interface{}{}
3169 if err = gulu.JSON.UnmarshalJSON(syaData, &syaJSON); err != nil {
3170 logging.LogErrorf("unmarshal file [%s] failed: %s", sya, err)
3171 return ast.WalkSkipChildren
3172 }
3173 annotationID := refID[strings.LastIndex(refID, "/")+1:]
3174 annotationData := syaJSON[annotationID]
3175 if nil == annotationData {
3176 logging.LogErrorf("not found annotation [%s] in .sya", annotationID)
3177 return ast.WalkSkipChildren
3178 }
3179 pages := annotationData.(map[string]interface{})["pages"].([]interface{})
3180 page := int(pages[0].(map[string]interface{})["index"].(float64)) + 1
3181 pageStr := strconv.Itoa(page)
3182
3183 refText := n.TextMarkTextContent
3184 ext := filepath.Ext(p)
3185 file := p[7:len(p)-23-len(ext)] + ext
3186 fileAnnotationRefLink := &ast.Node{Type: ast.NodeLink}
3187 fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
3188 if 0 == fileAnnotationRefMode {
3189 fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte(file + " - p" + pageStr + " - " + refText)})
3190 } else {
3191 fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte(refText)})
3192 }
3193 fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
3194 fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
3195 dest := p + "#page=" + pageStr // https://github.com/siyuan-note/siyuan/issues/11780
3196 fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(dest)})
3197 fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
3198 n.InsertBefore(fileAnnotationRefLink)
3199 return ast.WalkSkipChildren
3200}
3201
3202func exportPandocConvertZip(baseFolderName string, docPaths, defBlockIDs []string,
3203 pandocFrom, pandocTo, ext string, treeCache map[string]*parse.Tree) (zipPath string) {
3204 defer util.ClearPushProgress(100)
3205
3206 dir, name := path.Split(baseFolderName)
3207 name = util.FilterFileName(name)
3208 if strings.HasSuffix(name, "..") {
3209 // 文档标题以 `..` 结尾时无法导出 Markdown https://github.com/siyuan-note/siyuan/issues/4698
3210 // 似乎是 os.MkdirAll 的 bug,以 .. 结尾的路径无法创建,所以这里加上 _ 结尾
3211 name += "_"
3212 }
3213 baseFolderName = path.Join(dir, name)
3214
3215 exportFolder := filepath.Join(util.TempDir, "export", baseFolderName+ext)
3216 os.RemoveAll(exportFolder)
3217 if err := os.MkdirAll(exportFolder, 0755); err != nil {
3218 logging.LogErrorf("create export temp folder failed: %s", err)
3219 return
3220 }
3221
3222 exportRefMode := Conf.Export.BlockRefMode
3223 wrotePathHash := map[string]string{}
3224 assetsPathMap, err := allAssetAbsPaths()
3225 if nil != err {
3226 logging.LogWarnf("get assets abs path failed: %s", err)
3227 return
3228 }
3229
3230 luteEngine := util.NewLute()
3231 for i, p := range docPaths {
3232 id := util.GetTreeID(p)
3233 tree, md, isEmpty := exportMarkdownContent(id, ext, exportRefMode, defBlockIDs, false, treeCache)
3234 if nil == tree {
3235 continue
3236 }
3237 hPath := tree.HPath
3238 dir, name = path.Split(hPath)
3239 dir = util.FilterFilePath(dir) // 导出文档时未移除不支持的文件名符号 https://github.com/siyuan-note/siyuan/issues/4590
3240 name = util.FilterFileName(name)
3241 hPath = path.Join(dir, name)
3242 p = hPath + ext
3243 writePath := filepath.Join(exportFolder, p)
3244 hash := fmt.Sprintf("%x", sha1.Sum([]byte(md)))
3245 if gulu.File.IsExist(writePath) && hash != wrotePathHash[writePath] {
3246 // 重名文档加 ID
3247 p = hPath + "-" + id + ext
3248 writePath = filepath.Join(exportFolder, p)
3249 }
3250 writeFolder := filepath.Dir(writePath)
3251 if err := os.MkdirAll(writeFolder, 0755); err != nil {
3252 logging.LogErrorf("create export temp folder [%s] failed: %s", writeFolder, err)
3253 continue
3254 }
3255
3256 if isEmpty {
3257 entries, readErr := os.ReadDir(filepath.Join(util.DataDir, tree.Box, strings.TrimSuffix(tree.Path, ".sy")))
3258 if nil == readErr && 0 < len(entries) {
3259 // 如果文档内容为空并且存在子文档则仅导出文件夹
3260 // Improve export of empty documents with subdocuments https://github.com/siyuan-note/siyuan/issues/15009
3261 continue
3262 }
3263 }
3264
3265 // 解析导出后的标准 Markdown,汇总 assets
3266 tree = parse.Parse("", gulu.Str.ToBytes(md), luteEngine.ParseOptions)
3267 var assets []string
3268 assets = append(assets, getAssetsLinkDests(tree.Root)...)
3269 for _, asset := range assets {
3270 asset = string(html.DecodeDestination([]byte(asset)))
3271 if strings.Contains(asset, "?") {
3272 asset = asset[:strings.LastIndex(asset, "?")]
3273 }
3274
3275 if !strings.HasPrefix(asset, "assets/") {
3276 continue
3277 }
3278
3279 srcPath := assetsPathMap[asset]
3280 if "" == srcPath {
3281 logging.LogWarnf("get asset [%s] abs path failed", asset)
3282 continue
3283 }
3284
3285 destPath := filepath.Join(writeFolder, asset)
3286 if copyErr := filelock.Copy(srcPath, destPath); copyErr != nil {
3287 logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", srcPath, destPath, err)
3288 continue
3289 }
3290 }
3291
3292 // 调用 Pandoc 进行格式转换
3293 pandocErr := util.Pandoc(pandocFrom, pandocTo, writePath, md)
3294 if pandocErr != nil {
3295 logging.LogErrorf("pandoc failed: %s", pandocErr)
3296 continue
3297 }
3298
3299 wrotePathHash[writePath] = hash
3300 util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(70), fmt.Sprintf("%d/%d %s", i+1, len(docPaths), name)))
3301 }
3302
3303 zipPath = exportFolder + ".zip"
3304 zip, err := gulu.Zip.Create(zipPath)
3305 if err != nil {
3306 logging.LogErrorf("create export markdown zip [%s] failed: %s", exportFolder, err)
3307 return ""
3308 }
3309
3310 // 导出 Markdown zip 包内不带文件夹 https://github.com/siyuan-note/siyuan/issues/6869
3311 entries, err := os.ReadDir(exportFolder)
3312 if err != nil {
3313 logging.LogErrorf("read export markdown folder [%s] failed: %s", exportFolder, err)
3314 return ""
3315 }
3316
3317 zipCallback := func(filename string) {
3318 util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(253), filename))
3319 }
3320 for _, entry := range entries {
3321 entryName := entry.Name()
3322 entryPath := filepath.Join(exportFolder, entryName)
3323 if gulu.File.IsDir(entryPath) {
3324 err = zip.AddDirectory(entryName, entryPath, zipCallback)
3325 } else {
3326 err = zip.AddEntry(entryName, entryPath, zipCallback)
3327 }
3328 if err != nil {
3329 logging.LogErrorf("add entry [%s] to zip failed: %s", entryName, err)
3330 return ""
3331 }
3332 }
3333
3334 if err = zip.Close(); err != nil {
3335 logging.LogErrorf("close export markdown zip failed: %s", err)
3336 }
3337
3338 os.RemoveAll(exportFolder)
3339 zipPath = "/export/" + url.PathEscape(filepath.Base(zipPath))
3340 return
3341}
3342
3343func getExportBlockRefLinkText(blockRef *ast.Node, blockRefTextLeft, blockRefTextRight string) (defID, linkText string) {
3344 defID, linkText, _ = treenode.GetBlockRef(blockRef)
3345 if "" == linkText {
3346 linkText = sql.GetRefText(defID)
3347 }
3348 linkText = util.UnescapeHTML(linkText) // 块引锚文本导出时 `&` 变为实体 `&` https://github.com/siyuan-note/siyuan/issues/7659
3349 if Conf.Editor.BlockRefDynamicAnchorTextMaxLen < utf8.RuneCountInString(linkText) {
3350 linkText = gulu.Str.SubStr(linkText, Conf.Editor.BlockRefDynamicAnchorTextMaxLen) + "..."
3351 }
3352 linkText = blockRefTextLeft + linkText + blockRefTextRight
3353 return
3354}
3355
3356func prepareExportTrees(docPaths []string) (defBlockIDs []string, trees map[string]*parse.Tree, relatedDocPaths []string) {
3357 trees = map[string]*parse.Tree{}
3358 treeCache := map[string]*parse.Tree{}
3359 defBlockIDs = []string{}
3360 for i, p := range docPaths {
3361 rootID := strings.TrimSuffix(path.Base(p), ".sy")
3362 if !ast.IsNodeIDPattern(rootID) {
3363 continue
3364 }
3365
3366 tree, err := loadTreeWithCache(rootID, treeCache)
3367 if err != nil {
3368 continue
3369 }
3370 exportRefTrees(tree, &defBlockIDs, trees, treeCache)
3371
3372 util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(70), fmt.Sprintf("%d/%d %s", i+1, len(docPaths), tree.Root.IALAttr("title"))))
3373 }
3374
3375 for _, tree := range trees {
3376 relatedDocPaths = append(relatedDocPaths, tree.Path)
3377 }
3378 relatedDocPaths = gulu.Str.RemoveDuplicatedElem(relatedDocPaths)
3379 return
3380}
3381
3382func exportRefTrees(tree *parse.Tree, defBlockIDs *[]string, retTrees, treeCache map[string]*parse.Tree) {
3383 if nil != retTrees[tree.ID] {
3384 return
3385 }
3386 retTrees[tree.ID] = tree
3387
3388 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
3389 if !entering {
3390 return ast.WalkContinue
3391 }
3392
3393 if treenode.IsBlockRef(n) {
3394 defID, _, _ := treenode.GetBlockRef(n)
3395 if "" == defID {
3396 return ast.WalkContinue
3397 }
3398 defBlock := treenode.GetBlockTree(defID)
3399 if nil == defBlock {
3400 return ast.WalkSkipChildren
3401 }
3402
3403 var defTree *parse.Tree
3404 var err error
3405 if treeCache[defBlock.RootID] != nil {
3406 defTree = treeCache[defBlock.RootID]
3407 } else {
3408 defTree, err = loadTreeWithCache(defBlock.RootID, treeCache)
3409 if err != nil {
3410 return ast.WalkSkipChildren
3411 }
3412 treeCache[defBlock.RootID] = defTree
3413 }
3414 *defBlockIDs = append(*defBlockIDs, defID)
3415
3416 exportRefTrees(defTree, defBlockIDs, retTrees, treeCache)
3417 } else if treenode.IsBlockLink(n) {
3418 defID := strings.TrimPrefix(n.TextMarkAHref, "siyuan://blocks/")
3419 if "" == defID {
3420 return ast.WalkContinue
3421 }
3422 defBlock := treenode.GetBlockTree(defID)
3423 if nil == defBlock {
3424 return ast.WalkSkipChildren
3425 }
3426
3427 var defTree *parse.Tree
3428 var err error
3429 if treeCache[defBlock.RootID] != nil {
3430 defTree = treeCache[defBlock.RootID]
3431 } else {
3432 defTree, err = loadTreeWithCache(defBlock.RootID, treeCache)
3433 if err != nil {
3434 return ast.WalkSkipChildren
3435 }
3436 treeCache[defBlock.RootID] = defTree
3437 }
3438 *defBlockIDs = append(*defBlockIDs, defID)
3439
3440 exportRefTrees(defTree, defBlockIDs, retTrees, treeCache)
3441 } else if ast.NodeAttributeView == n.Type {
3442 // 导出数据库所在文档时一并导出绑定块所在文档
3443 // Export the binding block docs when exporting the doc where the database is located https://github.com/siyuan-note/siyuan/issues/11486
3444
3445 avID := n.AttributeViewID
3446 if "" == avID {
3447 return ast.WalkContinue
3448 }
3449
3450 attrView, _ := av.ParseAttributeView(avID)
3451 if nil == attrView {
3452 return ast.WalkContinue
3453 }
3454
3455 blockKeyValues := attrView.GetBlockKeyValues()
3456 if nil == blockKeyValues {
3457 return ast.WalkContinue
3458 }
3459
3460 for _, val := range blockKeyValues.Values {
3461 if val.IsDetached {
3462 continue
3463 }
3464
3465 defBlock := treenode.GetBlockTree(val.Block.ID)
3466 if nil == defBlock {
3467 continue
3468 }
3469
3470 var defTree *parse.Tree
3471 var err error
3472 if treeCache[defBlock.RootID] != nil {
3473 defTree = treeCache[defBlock.RootID]
3474 } else {
3475 defTree, err = loadTreeWithCache(defBlock.RootID, treeCache)
3476 if err != nil {
3477 continue
3478 }
3479 treeCache[defBlock.RootID] = defTree
3480 }
3481 *defBlockIDs = append(*defBlockIDs, val.BlockID)
3482
3483 exportRefTrees(defTree, defBlockIDs, retTrees, treeCache)
3484 }
3485 }
3486 return ast.WalkContinue
3487 })
3488
3489 *defBlockIDs = gulu.Str.RemoveDuplicatedElem(*defBlockIDs)
3490}
3491
3492func loadTreeWithCache(id string, treeCache map[string]*parse.Tree) (tree *parse.Tree, err error) {
3493 if tree = treeCache[id]; nil != tree {
3494 return
3495 }
3496 tree, err = LoadTreeByBlockID(id)
3497 if nil == err && nil != tree {
3498 treeCache[id] = tree
3499 }
3500 return
3501}
3502
3503func getAttrViewTable(attrView *av.AttributeView, view *av.View, query string) (ret *av.Table) {
3504 switch view.LayoutType {
3505 case av.LayoutTypeGallery:
3506 view.Table = av.NewLayoutTable()
3507 for _, field := range view.Gallery.CardFields {
3508 view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{BaseField: &av.BaseField{ID: field.ID}})
3509 }
3510 case av.LayoutTypeKanban:
3511 view.Table = av.NewLayoutTable()
3512 for _, field := range view.Kanban.Fields {
3513 view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{BaseField: &av.BaseField{ID: field.ID}})
3514 }
3515 }
3516
3517 depth := 1
3518 ret = sql.RenderAttributeViewTable(attrView, view, query, &depth, map[string]*av.AttributeView{})
3519 return
3520}
3521
3522// adjustHeadingLevel 聚焦导出(即非文档块)的情况下,将第一个标题层级提升为一级(如果开启了添加文档标题的话提升为二级)。
3523// Export preview mode supports focus use https://github.com/siyuan-note/siyuan/issues/15340
3524func adjustHeadingLevel(bt *treenode.BlockTree, tree *parse.Tree) {
3525 if "d" == bt.Type {
3526 return
3527 }
3528
3529 level := 1
3530 var firstHeading *ast.Node
3531 if !Conf.Export.AddTitle {
3532 for n := tree.Root.FirstChild; nil != n; n = n.Next {
3533 if ast.NodeHeading == n.Type && !n.ParentIs(ast.NodeBlockquote) {
3534 firstHeading = n
3535 break
3536 }
3537 }
3538 } else {
3539 for n := tree.Root.FirstChild.Next; nil != n; n = n.Next {
3540 if ast.NodeHeading == n.Type && !n.ParentIs(ast.NodeBlockquote) {
3541 firstHeading = n
3542 break
3543 }
3544 }
3545 level = 2
3546 }
3547 if nil != firstHeading {
3548 hLevel := firstHeading.HeadingLevel
3549 diff := level - hLevel
3550 var children, childrenHeadings []*ast.Node
3551 children = append(children, firstHeading)
3552 children = append(children, treenode.HeadingChildren(firstHeading)...)
3553 for _, c := range children {
3554 ccH := c.ChildrenByType(ast.NodeHeading)
3555 childrenHeadings = append(childrenHeadings, ccH...)
3556 }
3557 for _, h := range childrenHeadings {
3558 h.HeadingLevel += diff
3559 if 6 < h.HeadingLevel {
3560 h.HeadingLevel = 6
3561 } else if 1 > h.HeadingLevel {
3562 h.HeadingLevel = 1
3563 }
3564 }
3565 }
3566}