A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 3566 lines 115 kB view raw
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("![") 145 buf.WriteString(a.Name) 146 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) // 块引锚文本导出时 `&` 变为实体 `&amp;` 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}