2023-06-24 20:39:55 +08:00
// SiYuan - Refactor your thinking
2022-05-26 15:18:53 +08:00
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package model
import (
"bytes"
2024-12-16 22:50:55 +08:00
"crypto/sha1"
2024-01-03 23:46:09 +08:00
"encoding/csv"
2022-05-26 15:18:53 +08:00
"errors"
"fmt"
2023-01-02 21:28:50 +08:00
"net/http"
2022-05-26 15:18:53 +08:00
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
2022-12-17 11:53:11 +08:00
"time"
2022-05-26 15:18:53 +08:00
"unicode/utf8"
2024-09-16 16:41:50 +08:00
"github.com/88250/go-humanize"
2022-05-26 15:18:53 +08:00
"github.com/88250/gulu"
"github.com/88250/lute/ast"
2022-09-14 00:35:13 +08:00
"github.com/88250/lute/editor"
2022-05-26 15:18:53 +08:00
"github.com/88250/lute/html"
2024-07-11 09:48:36 +08:00
"github.com/88250/lute/lex"
2022-05-26 15:18:53 +08:00
"github.com/88250/lute/parse"
"github.com/88250/lute/render"
"github.com/emirpasic/gods/sets/hashset"
"github.com/emirpasic/gods/stacks/linkedliststack"
2023-01-27 17:56:29 +08:00
"github.com/imroc/req/v3"
2024-11-28 20:21:40 +08:00
"github.com/pdfcpu/pdfcpu/pkg/api"
"github.com/pdfcpu/pdfcpu/pkg/font"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types"
2022-06-15 23:56:47 +08:00
"github.com/siyuan-note/filelock"
2023-01-27 17:56:29 +08:00
"github.com/siyuan-note/httpclient"
2022-07-17 12:22:32 +08:00
"github.com/siyuan-note/logging"
2023-10-08 16:35:06 +08:00
"github.com/siyuan-note/riff"
2023-09-06 11:42:35 +08:00
"github.com/siyuan-note/siyuan/kernel/av"
2023-02-15 15:26:55 +08:00
"github.com/siyuan-note/siyuan/kernel/filesys"
2022-05-26 15:18:53 +08:00
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
2024-03-04 16:41:41 +08:00
func ExportAv2CSV ( avID , blockID string ) ( zipPath string , err error ) {
2024-01-04 11:52:43 +08:00
// Database block supports export as CSV https://github.com/siyuan-note/siyuan/issues/10072
2024-01-03 23:46:09 +08:00
attrView , err := av . ParseAttributeView ( avID )
2024-09-04 04:40:50 +03:00
if err != nil {
2024-01-03 23:46:09 +08:00
return
}
2024-03-04 16:41:41 +08:00
node , _ , err := getNodeByBlockID ( nil , blockID )
if nil == node {
return
}
viewID := node . IALAttr ( av . NodeAttrView )
2024-03-04 15:57:35 +08:00
view , err := attrView . GetCurrentView ( viewID )
2024-09-04 04:40:50 +03:00
if err != nil {
2024-01-03 23:46:09 +08:00
return
}
2024-05-20 23:04:03 +08:00
name := util . FilterFileName ( getAttrViewName ( attrView ) )
2024-10-17 23:31:54 +08:00
table := sql . RenderAttributeViewTable ( attrView , view , "" )
2024-01-03 23:46:09 +08:00
2024-02-29 22:49:02 +08:00
// 遵循视图过滤和排序规则 Use filtering and sorting of current view settings when exporting database blocks https://github.com/siyuan-note/siyuan/issues/10474
table . FilterRows ( attrView )
2024-03-21 10:03:08 +08:00
table . SortRows ( attrView )
2024-02-29 22:49:02 +08:00
2024-01-04 11:52:43 +08:00
exportFolder := filepath . Join ( util . TempDir , "export" , "csv" , name )
2024-09-04 04:40:50 +03:00
if err = os . MkdirAll ( exportFolder , 0755 ) ; err != nil {
2024-01-04 11:52:43 +08:00
logging . LogErrorf ( "mkdir [%s] failed: %s" , exportFolder , err )
2024-01-03 23:46:09 +08:00
return
}
2024-01-04 11:52:43 +08:00
csvPath := filepath . Join ( exportFolder , name + ".csv" )
2024-01-03 23:46:09 +08:00
f , err := os . OpenFile ( csvPath , os . O_RDWR | os . O_CREATE | os . O_TRUNC , 0644 )
2024-09-04 04:40:50 +03:00
if err != nil {
2024-01-03 23:46:09 +08:00
logging . LogErrorf ( "open [%s] failed: %s" , csvPath , err )
return
}
2024-09-04 04:40:50 +03:00
if _ , err = f . WriteString ( "\xEF\xBB\xBF" ) ; err != nil { // 写入 UTF-8 BOM, 避免使用 Microsoft Excel 打开乱码
2024-01-12 10:00:42 +08:00
logging . LogErrorf ( "write UTF-8 BOM to [%s] failed: %s" , csvPath , err )
f . Close ( )
return
}
2024-01-03 23:46:09 +08:00
2024-01-12 10:00:42 +08:00
writer := csv . NewWriter ( f )
2024-01-03 23:46:09 +08:00
var header [ ] string
for _ , col := range table . Columns {
header = append ( header , col . Name )
}
2024-09-04 04:40:50 +03:00
if err = writer . Write ( header ) ; err != nil {
2024-01-03 23:46:09 +08:00
logging . LogErrorf ( "write csv header [%s] failed: %s" , header , err )
2024-01-04 11:52:43 +08:00
f . Close ( )
2024-01-03 23:46:09 +08:00
return
}
2024-04-30 17:46:32 +08:00
rowNum := 1
2024-01-03 23:46:09 +08:00
for _ , row := range table . Rows {
var rowVal [ ] string
for _ , cell := range row . Cells {
var val string
if nil != cell . Value {
if av . KeyTypeDate == cell . Value . Type {
if nil != cell . Value . Date {
2024-02-23 17:53:43 +08:00
cell . Value . Date = av . NewFormattedValueDate ( cell . Value . Date . Content , cell . Value . Date . Content2 , av . DateFormatNone , cell . Value . Date . IsNotTime , cell . Value . Date . HasEndDate )
2024-01-03 23:46:09 +08:00
}
} else if av . KeyTypeCreated == cell . Value . Type {
if nil != cell . Value . Created {
cell . Value . Created = av . NewFormattedValueCreated ( cell . Value . Created . Content , 0 , av . CreatedFormatNone )
}
} else if av . KeyTypeUpdated == cell . Value . Type {
if nil != cell . Value . Updated {
cell . Value . Updated = av . NewFormattedValueUpdated ( cell . Value . Updated . Content , 0 , av . UpdatedFormatNone )
}
} else if av . KeyTypeMAsset == cell . Value . Type {
if nil != cell . Value . MAsset {
buf := & bytes . Buffer { }
for _ , a := range cell . Value . MAsset {
2024-04-30 17:46:32 +08:00
if av . AssetTypeImage == a . Type {
buf . WriteString ( "
buf . WriteString ( a . Content )
buf . WriteString ( ") " )
} else if av . AssetTypeFile == a . Type {
buf . WriteString ( "[" )
buf . WriteString ( a . Name )
buf . WriteString ( "](" )
buf . WriteString ( a . Content )
buf . WriteString ( ") " )
} else {
buf . WriteString ( a . Content )
buf . WriteString ( " " )
}
2024-01-03 23:46:09 +08:00
}
val = strings . TrimSpace ( buf . String ( ) )
}
2024-04-30 17:46:32 +08:00
} else if av . KeyTypeLineNumber == cell . Value . Type {
val = strconv . Itoa ( rowNum )
2024-01-03 23:46:09 +08:00
}
2024-04-30 17:46:32 +08:00
if "" == val {
val = cell . Value . String ( true )
}
2024-01-03 23:46:09 +08:00
}
rowVal = append ( rowVal , val )
}
2024-09-04 04:40:50 +03:00
if err = writer . Write ( rowVal ) ; err != nil {
2024-01-03 23:46:09 +08:00
logging . LogErrorf ( "write csv row [%s] failed: %s" , rowVal , err )
2024-01-04 11:52:43 +08:00
f . Close ( )
2024-01-03 23:46:09 +08:00
return
}
2024-04-30 17:46:32 +08:00
rowNum ++
2024-01-03 23:46:09 +08:00
}
writer . Flush ( )
2024-01-04 11:52:43 +08:00
zipPath = exportFolder + ".db.zip"
zip , err := gulu . Zip . Create ( zipPath )
2024-09-04 04:40:50 +03:00
if err != nil {
2024-01-04 11:52:43 +08:00
logging . LogErrorf ( "create export .db.zip [%s] failed: %s" , exportFolder , err )
f . Close ( )
return
}
2024-09-04 04:40:50 +03:00
if err = zip . AddDirectory ( "" , exportFolder ) ; err != nil {
2024-01-04 11:52:43 +08:00
logging . LogErrorf ( "create export .db.zip [%s] failed: %s" , exportFolder , err )
f . Close ( )
return
}
2024-09-04 04:40:50 +03:00
if err = zip . Close ( ) ; err != nil {
2024-01-04 11:52:43 +08:00
logging . LogErrorf ( "close export .db.zip failed: %s" , err )
f . Close ( )
return
}
f . Close ( )
removeErr := os . RemoveAll ( exportFolder )
if nil != removeErr {
logging . LogErrorf ( "remove export folder [%s] failed: %s" , exportFolder , removeErr )
}
2024-01-04 21:41:30 +08:00
zipPath = "/export/csv/" + url . PathEscape ( filepath . Base ( zipPath ) )
2024-01-03 23:46:09 +08:00
return
}
2023-01-02 21:28:50 +08:00
func Export2Liandi ( id string ) ( err error ) {
2024-03-10 23:27:13 +08:00
tree , err := LoadTreeByBlockID ( id )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-01-02 21:28:50 +08:00
logging . LogErrorf ( "load tree by block id [%s] failed: %s" , id , err )
return
}
2023-05-29 14:27:18 +08:00
if IsUserGuide ( tree . Box ) {
2023-05-29 14:27:43 +08:00
// Doc in the user guide no longer supports one-click sending to the community https://github.com/siyuan-note/siyuan/issues/8388
2023-05-29 14:27:18 +08:00
return errors . New ( Conf . Language ( 204 ) )
}
2024-01-01 16:54:00 +08:00
assets := assetsLinkDestsInTree ( tree )
2024-01-01 17:07:15 +08:00
embedAssets := assetsLinkDestsInQueryEmbedNodes ( tree )
assets = append ( assets , embedAssets ... )
assets = gulu . Str . RemoveDuplicatedElem ( assets )
2024-05-14 00:05:09 +08:00
_ , err = uploadAssets2Cloud ( assets , bizTypeExport2Liandi )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-01-02 23:26:01 +08:00
return
}
2023-01-02 21:36:44 +08:00
2023-01-02 22:27:57 +08:00
msgId := util . PushMsg ( Conf . Language ( 182 ) , 15000 )
2023-01-02 23:26:01 +08:00
defer util . PushClearMsg ( msgId )
2023-01-02 22:27:57 +08:00
2023-01-02 21:28:50 +08:00
// 判断帖子是否已经存在,存在则使用更新接口
2023-01-03 00:19:41 +08:00
const liandiArticleIdAttrName = "custom-liandi-articleId"
2023-01-02 21:28:50 +08:00
foundArticle := false
2023-01-03 00:19:41 +08:00
articleId := tree . Root . IALAttr ( liandiArticleIdAttrName )
2023-01-02 21:28:50 +08:00
if "" != articleId {
2023-01-09 22:11:41 +08:00
result := gulu . Ret . NewResult ( )
2023-01-02 21:28:50 +08:00
request := httpclient . NewCloudRequest30s ( )
resp , getErr := request .
2023-02-17 11:00:04 +08:00
SetSuccessResult ( result ) .
2023-12-08 21:46:46 +08:00
SetCookies ( & http . Cookie { Name : "symphony" , Value : Conf . GetUser ( ) . UserToken } ) .
2023-06-20 11:48:44 +08:00
Get ( util . GetCloudAccountServer ( ) + "/api/v2/article/update/" + articleId )
2023-01-02 21:28:50 +08:00
if nil != getErr {
logging . LogErrorf ( "get liandi article info failed: %s" , getErr )
return getErr
}
switch resp . StatusCode {
case 200 :
2023-01-09 22:11:41 +08:00
if 0 == result . Code {
foundArticle = true
} else if 1 == result . Code {
foundArticle = false
}
2023-01-02 21:28:50 +08:00
case 404 :
foundArticle = false
default :
2024-10-19 11:43:53 +08:00
err = errors . New ( fmt . Sprintf ( "get liandi article info failed [sc=%d]" , resp . StatusCode ) )
2023-01-02 21:28:50 +08:00
return
}
}
2023-06-20 11:48:44 +08:00
apiURL := util . GetCloudAccountServer ( ) + "/api/v2/article"
2023-01-02 21:28:50 +08:00
if foundArticle {
apiURL += "/" + articleId
}
title := path . Base ( tree . HPath )
tags := tree . Root . IALAttr ( "tags" )
2023-12-08 21:46:46 +08:00
content := exportMarkdownContent0 ( tree , util . GetCloudForumAssetsServer ( ) + time . Now ( ) . Format ( "2006/01" ) + "/siyuan/" + Conf . GetUser ( ) . UserId + "/" , true ,
2025-02-16 14:25:31 +08:00
".md" , 3 , 1 , 1 ,
2023-01-02 23:26:01 +08:00
"#" , "#" ,
"" , "" ,
2025-04-20 17:27:14 +08:00
false , false , nil , true , & map [ string ] * parse . Tree { } )
2023-01-09 22:11:41 +08:00
result := gulu . Ret . NewResult ( )
2023-01-02 21:28:50 +08:00
request := httpclient . NewCloudRequest30s ( )
request = request .
2023-02-17 11:00:04 +08:00
SetSuccessResult ( result ) .
2023-12-08 21:46:46 +08:00
SetCookies ( & http . Cookie { Name : "symphony" , Value : Conf . GetUser ( ) . UserToken } ) .
2023-01-02 21:28:50 +08:00
SetBody ( map [ string ] interface { } {
2023-01-02 22:02:23 +08:00
"articleTitle" : title ,
"articleTags" : tags ,
"articleContent" : content } )
2023-01-02 21:28:50 +08:00
var resp * req . Response
var sendErr error
if foundArticle {
resp , sendErr = request . Put ( apiURL )
} else {
resp , sendErr = request . Post ( apiURL )
}
if nil != sendErr {
logging . LogErrorf ( "send article to liandi failed: %s" , err )
return err
}
if 200 != resp . StatusCode {
msg := fmt . Sprintf ( "send article to liandi failed [sc=%d]" , resp . StatusCode )
logging . LogErrorf ( msg )
return errors . New ( msg )
}
2023-01-02 22:02:23 +08:00
if 0 != result . Code {
msg := fmt . Sprintf ( "send article to liandi failed [code=%d, msg=%s]" , result . Code , result . Msg )
logging . LogErrorf ( msg )
2023-01-02 23:26:01 +08:00
util . PushClearMsg ( msgId )
2023-01-02 22:22:11 +08:00
return errors . New ( result . Msg )
2023-01-02 22:02:23 +08:00
}
2023-01-02 21:28:50 +08:00
if ! foundArticle {
2023-01-02 22:00:20 +08:00
articleId = result . Data . ( string )
2024-03-10 23:27:13 +08:00
tree , _ = LoadTreeByBlockID ( id ) // 这里必须重新加载,因为前面导出时已经修改了树结构
2023-01-03 00:19:41 +08:00
tree . Root . SetIALAttr ( liandiArticleIdAttrName , articleId )
2024-09-04 04:40:50 +03:00
if err = writeTreeUpsertQueue ( tree ) ; err != nil {
2023-01-02 21:28:50 +08:00
return
}
}
2024-10-19 11:43:53 +08:00
util . PushMsg ( fmt . Sprintf ( Conf . Language ( 181 ) , util . GetCloudAccountServer ( ) + "/article/" + articleId ) , 7000 )
2023-01-02 21:28:50 +08:00
return
}
2022-08-27 01:19:52 +08:00
func ExportSystemLog ( ) ( zipPath string ) {
exportFolder := filepath . Join ( util . TempDir , "export" , "system-log" )
os . RemoveAll ( exportFolder )
2024-09-04 04:40:50 +03:00
if err := os . MkdirAll ( exportFolder , 0755 ) ; err != nil {
2022-08-27 01:19:52 +08:00
logging . LogErrorf ( "create export temp folder failed: %s" , err )
return
}
appLog := filepath . Join ( util . HomeDir , ".config" , "siyuan" , "app.log" )
if gulu . File . IsExist ( appLog ) {
to := filepath . Join ( exportFolder , "app.log" )
2024-09-04 04:40:50 +03:00
if err := filelock . Copy ( appLog , to ) ; err != nil {
2022-08-27 01:19:52 +08:00
logging . LogErrorf ( "copy app log from [%s] to [%s] failed: %s" , err , appLog , to )
}
}
2023-03-27 11:47:38 +08:00
kernelLog := filepath . Join ( util . HomeDir , ".config" , "siyuan" , "kernel.log" )
2022-08-27 01:19:52 +08:00
if gulu . File . IsExist ( kernelLog ) {
2023-03-27 11:47:38 +08:00
to := filepath . Join ( exportFolder , "kernel.log" )
2024-09-04 04:40:50 +03:00
if err := filelock . Copy ( kernelLog , to ) ; err != nil {
2022-08-27 01:19:52 +08:00
logging . LogErrorf ( "copy kernel log from [%s] to [%s] failed: %s" , err , kernelLog , to )
}
}
2023-03-27 11:47:38 +08:00
siyuanLog := filepath . Join ( util . TempDir , "siyuan.log" )
if gulu . File . IsExist ( siyuanLog ) {
to := filepath . Join ( exportFolder , "siyuan.log" )
2024-09-04 04:40:50 +03:00
if err := filelock . Copy ( siyuanLog , to ) ; err != nil {
2023-03-27 11:47:38 +08:00
logging . LogErrorf ( "copy kernel log from [%s] to [%s] failed: %s" , err , siyuanLog , to )
}
}
2024-01-28 14:28:46 +08:00
mobileLog := filepath . Join ( util . TempDir , "mobile.log" )
if gulu . File . IsExist ( mobileLog ) {
to := filepath . Join ( exportFolder , "mobile.log" )
2024-09-04 04:40:50 +03:00
if err := filelock . Copy ( mobileLog , to ) ; err != nil {
2024-01-28 14:28:46 +08:00
logging . LogErrorf ( "copy mobile log from [%s] to [%s] failed: %s" , err , mobileLog , to )
}
}
2022-08-27 01:19:52 +08:00
zipPath = exportFolder + ".zip"
zip , err := gulu . Zip . Create ( zipPath )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-08-27 01:19:52 +08:00
logging . LogErrorf ( "create export log zip [%s] failed: %s" , exportFolder , err )
return ""
}
2024-09-04 04:40:50 +03:00
if err = zip . AddDirectory ( "log" , exportFolder ) ; err != nil {
2022-08-27 01:19:52 +08:00
logging . LogErrorf ( "create export log zip [%s] failed: %s" , exportFolder , err )
return ""
}
2024-09-04 04:40:50 +03:00
if err = zip . Close ( ) ; err != nil {
2022-08-27 01:19:52 +08:00
logging . LogErrorf ( "close export log zip failed: %s" , err )
}
os . RemoveAll ( exportFolder )
zipPath = "/export/" + url . PathEscape ( filepath . Base ( zipPath ) )
return
}
2022-07-24 22:05:14 +08:00
func ExportNotebookSY ( id string ) ( zipPath string ) {
zipPath = exportBoxSYZip ( id )
return
}
2022-05-26 15:18:53 +08:00
func ExportSY ( id string ) ( name , zipPath string ) {
block := treenode . GetBlockTree ( id )
if nil == block {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "not found block [%s]" , id )
2022-05-26 15:18:53 +08:00
return
}
boxID := block . BoxID
box := Conf . Box ( boxID )
baseFolderName := path . Base ( block . HPath )
if "." == baseFolderName {
baseFolderName = path . Base ( block . Path )
}
rootPath := block . Path
docPaths := [ ] string { rootPath }
docFiles := box . ListFiles ( strings . TrimSuffix ( block . Path , ".sy" ) )
for _ , docFile := range docFiles {
docPaths = append ( docPaths , docFile . path )
}
zipPath = exportSYZip ( boxID , path . Dir ( rootPath ) , baseFolderName , docPaths )
2024-11-29 08:41:43 +08:00
name = util . GetTreeID ( block . Path )
2022-05-26 15:18:53 +08:00
return
}
2022-12-07 16:38:49 +08:00
func ExportDataInFolder ( exportFolder string ) ( name string , err error ) {
2022-05-26 15:18:53 +08:00
util . PushEndlessProgress ( Conf . Language ( 65 ) )
defer util . ClearPushProgress ( 100 )
2024-09-16 16:41:50 +08:00
data := filepath . Join ( util . WorkspaceDir , "data" )
if util . ContainerStd == util . Container {
// 桌面端检查磁盘可用空间
dataSize , sizeErr := util . SizeOfDirectory ( data )
if sizeErr != nil {
logging . LogErrorf ( "get size of data dir [%s] failed: %s" , data , sizeErr )
err = sizeErr
return
}
_ , _ , tempExportFree := util . GetDiskUsage ( util . TempDir )
if int64 ( tempExportFree ) < dataSize * 2 { // 压缩 zip 文件时需要 data 的两倍空间
err = errors . New ( fmt . Sprintf ( Conf . Language ( 242 ) , humanize . BytesCustomCeil ( tempExportFree , 2 ) , humanize . BytesCustomCeil ( uint64 ( dataSize ) * 2 , 2 ) ) )
return
}
_ , _ , targetExportFree := util . GetDiskUsage ( exportFolder )
if int64 ( targetExportFree ) < dataSize { // 复制 zip 最多需要 data 一样的空间
err = errors . New ( fmt . Sprintf ( Conf . Language ( 242 ) , humanize . BytesCustomCeil ( targetExportFree , 2 ) , humanize . BytesCustomCeil ( uint64 ( dataSize ) , 2 ) ) )
return
}
}
2022-12-31 16:30:24 +08:00
zipPath , err := ExportData ( )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-05-26 15:18:53 +08:00
return
}
2022-12-07 16:38:49 +08:00
name = filepath . Base ( zipPath )
2023-07-04 20:19:26 +08:00
name , err = url . PathUnescape ( name )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-07-04 20:19:26 +08:00
logging . LogErrorf ( "url unescape [%s] failed: %s" , name , err )
return
}
2024-09-16 16:41:50 +08:00
util . PushEndlessProgress ( Conf . Language ( 65 ) )
defer util . ClearPushProgress ( 100 )
2022-12-31 16:30:24 +08:00
targetZipPath := filepath . Join ( exportFolder , name )
zipAbsPath := filepath . Join ( util . TempDir , "export" , name )
2023-06-15 09:41:42 +08:00
err = filelock . Copy ( zipAbsPath , targetZipPath )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-12-31 16:30:24 +08:00
logging . LogErrorf ( "copy export zip from [%s] to [%s] failed: %s" , zipAbsPath , targetZipPath , err )
return
}
if removeErr := os . Remove ( zipAbsPath ) ; nil != removeErr {
logging . LogErrorf ( "remove export zip failed: %s" , removeErr )
}
2022-05-26 15:18:53 +08:00
return
}
2022-12-31 16:30:24 +08:00
func ExportData ( ) ( zipPath string , err error ) {
2022-05-26 15:18:53 +08:00
util . PushEndlessProgress ( Conf . Language ( 65 ) )
defer util . ClearPushProgress ( 100 )
2024-09-08 10:00:09 +08:00
name := util . FilterFileName ( util . WorkspaceName ) + "-" + util . CurrentTimeSecondsStr ( )
2023-06-17 00:15:24 +08:00
exportFolder := filepath . Join ( util . TempDir , "export" , name )
2022-12-31 16:30:24 +08:00
zipPath , err = exportData ( exportFolder )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-05-26 15:18:53 +08:00
return
}
zipPath = "/export/" + url . PathEscape ( filepath . Base ( zipPath ) )
return
}
2022-12-07 16:38:49 +08:00
func exportData ( exportFolder string ) ( zipPath string , err error ) {
2024-10-22 19:20:44 +08:00
FlushTxQueue ( )
2022-12-31 16:30:24 +08:00
2024-09-14 22:49:01 +08:00
logging . LogInfof ( "exporting data..." )
2022-05-26 15:18:53 +08:00
baseFolderName := "data-" + util . CurrentTimeSecondsStr ( )
2024-09-04 04:40:50 +03:00
if err = os . MkdirAll ( exportFolder , 0755 ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "create export temp folder failed: %s" , err )
2022-05-26 15:18:53 +08:00
return
}
data := filepath . Join ( util . WorkspaceDir , "data" )
2024-09-04 04:40:50 +03:00
if err = filelock . Copy ( data , exportFolder ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "copy data dir from [%s] to [%s] failed: %s" , data , baseFolderName , err )
2023-04-06 15:00:03 +08:00
err = errors . New ( fmt . Sprintf ( Conf . Language ( 14 ) , err . Error ( ) ) )
2022-05-26 15:18:53 +08:00
return
}
2022-12-07 16:38:49 +08:00
zipPath = exportFolder + ".zip"
2022-05-26 15:18:53 +08:00
zip , err := gulu . Zip . Create ( zipPath )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "create export data zip [%s] failed: %s" , exportFolder , err )
2022-05-26 15:18:53 +08:00
return
}
2024-06-12 12:03:31 +08:00
zipCallback := func ( filename string ) {
2025-01-01 11:02:16 +08:00
util . PushEndlessProgress ( Conf . language ( 65 ) + " " + fmt . Sprintf ( Conf . language ( 253 ) , filename ) )
2024-06-12 12:03:31 +08:00
}
2024-09-04 04:40:50 +03:00
if err = zip . AddDirectory ( baseFolderName , exportFolder , zipCallback ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "create export data zip [%s] failed: %s" , exportFolder , err )
2022-05-26 15:18:53 +08:00
return
}
2024-09-04 04:40:50 +03:00
if err = zip . Close ( ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "close export data zip failed: %s" , err )
2022-05-26 15:18:53 +08:00
}
os . RemoveAll ( exportFolder )
2024-09-14 22:49:01 +08:00
logging . LogInfof ( "export data done [%s]" , zipPath )
2022-05-26 15:18:53 +08:00
return
}
2023-07-28 16:48:17 +08:00
func ExportResources ( resourcePaths [ ] string , mainName string ) ( exportFilePath string , err error ) {
2024-10-22 19:20:44 +08:00
FlushTxQueue ( )
2023-07-28 16:48:17 +08:00
// 用于导出的临时文件夹完整路径
exportFolderPath := filepath . Join ( util . TempDir , "export" , mainName )
2024-09-04 04:40:50 +03:00
if err = os . MkdirAll ( exportFolderPath , 0755 ) ; err != nil {
2023-07-28 16:48:17 +08:00
logging . LogErrorf ( "create export temp folder failed: %s" , err )
return
}
// 将需要导出的文件/文件夹复制到临时文件夹
for _ , resourcePath := range resourcePaths {
2024-12-11 17:15:54 +08:00
resourceFullPath := filepath . Join ( util . WorkspaceDir , resourcePath ) // 资源完整路径
if ! util . IsAbsPathInWorkspace ( resourceFullPath ) {
logging . LogErrorf ( "resource path [%s] is not in workspace" , resourceFullPath )
err = errors . New ( "resource path [" + resourcePath + "] is not in workspace" )
return
}
2023-07-28 16:48:17 +08:00
resourceBaseName := filepath . Base ( resourceFullPath ) // 资源名称
resourceCopyPath := filepath . Join ( exportFolderPath , resourceBaseName ) // 资源副本完整路径
2024-09-04 04:40:50 +03:00
if err = filelock . Copy ( resourceFullPath , resourceCopyPath ) ; err != nil {
2023-07-28 16:48:17 +08:00
logging . LogErrorf ( "copy resource will be exported from [%s] to [%s] failed: %s" , resourcePath , resourceCopyPath , err )
err = fmt . Errorf ( Conf . Language ( 14 ) , err . Error ( ) )
return
}
}
zipFilePath := exportFolderPath + ".zip" // 导出的 *.zip 文件完整路径
zip , err := gulu . Zip . Create ( zipFilePath )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-07-28 16:48:17 +08:00
logging . LogErrorf ( "create export zip [%s] failed: %s" , zipFilePath , err )
return
}
2024-09-04 04:40:50 +03:00
if err = zip . AddDirectory ( mainName , exportFolderPath ) ; err != nil {
2023-07-28 16:48:17 +08:00
logging . LogErrorf ( "create export zip [%s] failed: %s" , exportFolderPath , err )
return
}
2024-09-04 04:40:50 +03:00
if err = zip . Close ( ) ; err != nil {
2023-07-28 16:48:17 +08:00
logging . LogErrorf ( "close export zip failed: %s" , err )
}
os . RemoveAll ( exportFolderPath )
exportFilePath = path . Join ( "temp" , "export" , mainName + ".zip" ) // 导出的 *.zip 文件相对于工作区目录的路径
return
}
2024-09-18 11:28:04 +08:00
func Preview ( id string ) ( retStdHTML string ) {
2024-12-02 11:50:32 +08:00
blockRefMode := Conf . Export . BlockRefMode
2024-03-10 23:27:13 +08:00
tree , _ := LoadTreeByBlockID ( id )
2024-08-11 09:33:43 +08:00
tree = exportTree ( tree , false , false , true ,
2024-12-02 11:50:32 +08:00
blockRefMode , Conf . Export . BlockEmbedMode , Conf . Export . FileAnnotationRefMode ,
2025-02-09 11:19:26 +08:00
"#" , "#" , // 这里固定使用 # 包裹标签,否则无法正确解析标签 https://github.com/siyuan-note/siyuan/issues/13857
2023-01-02 23:26:01 +08:00
Conf . Export . BlockRefTextLeft , Conf . Export . BlockRefTextRight ,
2025-04-20 17:27:14 +08:00
Conf . Export . AddTitle , Conf . Export . InlineMemo , true , true , & map [ string ] * parse . Tree { } )
2022-05-26 15:18:53 +08:00
luteEngine := NewLute ( )
2025-02-09 11:19:26 +08:00
enableLuteInlineSyntax ( luteEngine )
2022-05-26 15:18:53 +08:00
luteEngine . SetFootnotes ( true )
2024-05-13 23:39:16 +08:00
addBlockIALNodes ( tree , false )
2025-03-13 20:40:16 +08:00
// 移除超级块的属性列表 https://github.com/siyuan-note/siyuan/issues/13451
var unlinks [ ] * ast . Node
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if entering && ast . NodeKramdownBlockIAL == n . Type && nil != n . Previous && ast . NodeSuperBlock == n . Previous . Type {
unlinks = append ( unlinks , n )
}
return ast . WalkContinue
} )
for _ , unlink := range unlinks {
unlink . Unlink ( )
}
2022-05-26 15:18:53 +08:00
md := treenode . FormatNode ( tree . Root , luteEngine )
tree = parse . Parse ( "" , [ ] byte ( md ) , luteEngine . ParseOptions )
2024-05-21 15:35:39 +08:00
// 使用实际主题样式值替换样式变量 Use real theme style value replace var in preview mode https://github.com/siyuan-note/siyuan/issues/11458
fillThemeStyleVar ( tree )
2025-02-11 18:19:42 +08:00
luteEngine . RenderOptions . ProtyleMarkNetImg = false
2023-06-17 15:49:16 +08:00
retStdHTML = luteEngine . ProtylePreview ( tree , luteEngine . RenderOptions )
if footnotesDefBlock := tree . Root . ChildByType ( ast . NodeFootnotesDefBlock ) ; nil != footnotesDefBlock {
footnotesDefBlock . Unlink ( )
}
return
2022-05-26 15:18:53 +08:00
}
2024-05-19 17:55:06 +08:00
func ExportDocx ( id , savePath string , removeAssets , merge bool ) ( fullPath string , err error ) {
2022-05-26 15:18:53 +08:00
if ! util . IsValidPandocBin ( Conf . Export . PandocBin ) {
2023-06-15 10:15:13 +08:00
Conf . Export . PandocBin = util . PandocBinPath
Conf . Save ( )
if ! util . IsValidPandocBin ( Conf . Export . PandocBin ) {
2024-05-19 17:55:06 +08:00
err = errors . New ( Conf . Language ( 115 ) )
return
2023-06-15 10:15:13 +08:00
}
2022-05-26 15:18:53 +08:00
}
tmpDir := filepath . Join ( util . TempDir , "export" , gulu . Rand . String ( 7 ) )
2024-09-04 04:40:50 +03:00
if err = os . MkdirAll ( tmpDir , 0755 ) ; err != nil {
2022-09-01 15:16:17 +08:00
return
}
2022-05-26 15:18:53 +08:00
defer os . Remove ( tmpDir )
2022-12-10 18:23:12 +08:00
name , content := ExportMarkdownHTML ( id , tmpDir , true , merge )
2024-12-21 17:52:31 +08:00
content = strings . ReplaceAll ( content , " \n" , "<br>\n" )
2022-09-01 15:16:17 +08:00
2022-05-26 15:18:53 +08:00
tmpDocxPath := filepath . Join ( tmpDir , name + ".docx" )
args := [ ] string { // pandoc -f html --resource-path=请从这里开始 请从这里开始\index.html -o test.docx
2022-09-01 15:16:17 +08:00
"-f" , "html+tex_math_dollars" ,
2022-05-26 15:18:53 +08:00
"--resource-path" , tmpDir ,
"-o" , tmpDocxPath ,
}
2023-09-09 16:38:03 +08:00
// Pandoc template for exporting docx https://github.com/siyuan-note/siyuan/issues/8740
2024-11-27 20:13:22 +08:00
docxTemplate := util . RemoveInvalid ( Conf . Export . DocxTemplate )
2023-09-09 16:38:03 +08:00
docxTemplate = strings . TrimSpace ( docxTemplate )
if "" != docxTemplate {
if ! gulu . File . IsExist ( docxTemplate ) {
logging . LogErrorf ( "docx template [%s] not found" , docxTemplate )
2024-10-19 11:43:53 +08:00
err = errors . New ( fmt . Sprintf ( Conf . Language ( 197 ) , docxTemplate ) )
2024-05-19 17:55:06 +08:00
return
2023-09-09 16:38:03 +08:00
}
args = append ( args , "--reference-doc" , docxTemplate )
}
2022-05-26 15:18:53 +08:00
pandoc := exec . Command ( Conf . Export . PandocBin , args ... )
2022-09-29 21:52:01 +08:00
gulu . CmdAttr ( pandoc )
2022-09-01 15:16:17 +08:00
pandoc . Stdin = bytes . NewBufferString ( content )
2022-05-26 15:18:53 +08:00
output , err := pandoc . CombinedOutput ( )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "export docx failed: %s" , gulu . Str . FromBytes ( output ) )
2024-10-19 11:43:53 +08:00
err = errors . New ( fmt . Sprintf ( Conf . Language ( 14 ) , gulu . Str . FromBytes ( output ) ) )
2024-05-19 17:55:06 +08:00
return
2023-07-26 16:56:40 +08:00
}
2024-05-19 17:55:06 +08:00
fullPath = filepath . Join ( savePath , name + ".docx" )
fullPath = util . GetUniqueFilename ( fullPath )
2024-09-04 04:40:50 +03:00
if err = filelock . Copy ( tmpDocxPath , fullPath ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "export docx failed: %s" , err )
2024-05-19 17:55:06 +08:00
err = errors . New ( fmt . Sprintf ( Conf . Language ( 14 ) , err ) )
return
2022-05-26 15:18:53 +08:00
}
2022-07-20 10:43:02 +08:00
if tmpAssets := filepath . Join ( tmpDir , "assets" ) ; ! removeAssets && gulu . File . IsDir ( tmpAssets ) {
2024-09-04 04:40:50 +03:00
if err = filelock . Copy ( tmpAssets , filepath . Join ( savePath , "assets" ) ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "export docx failed: %s" , err )
2024-05-19 17:55:06 +08:00
err = errors . New ( fmt . Sprintf ( Conf . Language ( 14 ) , err ) )
return
2022-05-26 15:18:53 +08:00
}
}
return
}
2022-12-10 18:23:12 +08:00
func ExportMarkdownHTML ( id , savePath string , docx , merge bool ) ( name , dom string ) {
2023-02-15 15:26:55 +08:00
bt := treenode . GetBlockTree ( id )
if nil == bt {
return
}
2023-02-15 17:59:57 +08:00
tree := prepareExportTree ( bt )
2022-05-26 15:18:53 +08:00
2022-12-10 18:23:12 +08:00
if merge {
var mergeErr error
tree , mergeErr = mergeSubDocs ( tree )
if nil != mergeErr {
logging . LogErrorf ( "merge sub docs failed: %s" , mergeErr )
return
}
}
2024-12-01 21:37:31 +08:00
blockRefMode := Conf . Export . BlockRefMode
2024-08-11 09:33:43 +08:00
tree = exportTree ( tree , true , false , true ,
2024-12-01 21:37:31 +08:00
blockRefMode , Conf . Export . BlockEmbedMode , Conf . Export . FileAnnotationRefMode ,
2023-01-02 23:26:01 +08:00
Conf . Export . TagOpenMarker , Conf . Export . TagCloseMarker ,
Conf . Export . BlockRefTextLeft , Conf . Export . BlockRefTextRight ,
2025-04-20 17:27:14 +08:00
Conf . Export . AddTitle , Conf . Export . InlineMemo , true , true , & map [ string ] * parse . Tree { } )
2022-05-26 15:18:53 +08:00
name = path . Base ( tree . HPath )
2022-08-11 23:52:50 +08:00
name = util . FilterFileName ( name ) // 导出 PDF、HTML 和 Word 时未移除不支持的文件名符号 https://github.com/siyuan-note/siyuan/issues/5614
2022-11-23 17:00:51 +08:00
savePath = strings . TrimSpace ( savePath )
2022-05-26 15:18:53 +08:00
2024-09-04 04:40:50 +03:00
if err := os . MkdirAll ( savePath , 0755 ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "mkdir [%s] failed: %s" , savePath , err )
2022-05-26 15:18:53 +08:00
return
}
assets := assetsLinkDestsInTree ( tree )
for _ , asset := range assets {
if strings . HasPrefix ( asset , "assets/" ) {
2023-04-12 12:02:56 +08:00
if strings . Contains ( asset , "?" ) {
asset = asset [ : strings . LastIndex ( asset , "?" ) ]
}
2022-05-26 15:18:53 +08:00
srcAbsPath , err := GetAssetAbsPath ( asset )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-07-17 12:22:32 +08:00
logging . LogWarnf ( "resolve path of asset [%s] failed: %s" , asset , err )
2022-05-26 15:18:53 +08:00
continue
}
targetAbsPath := filepath . Join ( savePath , asset )
2024-09-04 04:40:50 +03:00
if err = filelock . Copy ( srcAbsPath , targetAbsPath ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogWarnf ( "copy asset from [%s] to [%s] failed: %s" , srcAbsPath , targetAbsPath , err )
2022-05-26 15:18:53 +08:00
}
}
}
2024-12-23 16:14:16 +08:00
srcs := [ ] string { "stage/build/export" , "stage/protyle" }
2022-05-26 15:18:53 +08:00
for _ , src := range srcs {
from := filepath . Join ( util . WorkingDir , src )
to := filepath . Join ( savePath , src )
2024-09-04 04:40:50 +03:00
if err := filelock . Copy ( from , to ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogWarnf ( "copy stage from [%s] to [%s] failed: %s" , from , savePath , err )
2022-05-26 15:18:53 +08:00
}
}
theme := Conf . Appearance . ThemeLight
if 1 == Conf . Appearance . Mode {
theme = Conf . Appearance . ThemeDark
}
srcs = [ ] string { "icons" , "themes/" + theme }
2023-09-13 08:54:29 +08:00
appearancePath := util . AppearancePath
if util . IsSymlinkPath ( util . AppearancePath ) {
// Support for symlinked theme folder when exporting HTML https://github.com/siyuan-note/siyuan/issues/9173
var readErr error
2023-09-13 08:58:25 +08:00
appearancePath , readErr = filepath . EvalSymlinks ( util . AppearancePath )
2023-09-13 08:54:29 +08:00
if nil != readErr {
logging . LogErrorf ( "readlink [%s] failed: %s" , util . AppearancePath , readErr )
return
}
}
2022-05-26 15:18:53 +08:00
for _ , src := range srcs {
2023-09-13 08:54:29 +08:00
from := filepath . Join ( appearancePath , src )
2022-05-26 15:18:53 +08:00
to := filepath . Join ( savePath , "appearance" , src )
2024-09-04 04:40:50 +03:00
if err := filelock . Copy ( from , to ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "copy appearance from [%s] to [%s] failed: %s" , from , savePath , err )
2022-05-26 15:18:53 +08:00
return
}
}
2022-07-28 23:47:55 +08:00
// 复制自定义表情图片
2022-07-29 23:37:04 +08:00
emojis := emojisInTree ( tree )
for _ , emoji := range emojis {
from := filepath . Join ( util . DataDir , emoji )
to := filepath . Join ( savePath , emoji )
2024-09-04 04:40:50 +03:00
if err := filelock . Copy ( from , to ) ; err != nil {
2024-12-19 23:51:12 +08:00
logging . LogErrorf ( "copy emojis from [%s] to [%s] failed: %s" , from , to , err )
2022-07-29 23:37:04 +08:00
}
2022-07-28 23:47:55 +08:00
}
2022-05-26 15:18:53 +08:00
if docx {
processIFrame ( tree )
}
2024-12-02 11:50:32 +08:00
luteEngine := NewLute ( )
luteEngine . SetFootnotes ( true )
2022-07-29 23:37:04 +08:00
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if ast . NodeEmojiImg == n . Type {
2024-12-04 18:08:47 +08:00
// 自定义表情图片地址去掉开头的 /
2022-07-29 23:37:04 +08:00
n . Tokens = bytes . ReplaceAll ( n . Tokens , [ ] byte ( "src=\"/emojis" ) , [ ] byte ( "src=\"emojis" ) )
2024-12-04 18:08:47 +08:00
} else if ast . NodeList == n . Type {
if nil != n . ListData && 1 == n . ListData . Typ {
if 0 == n . ListData . Start {
n . ListData . Start = 1
}
if li := n . ChildByType ( ast . NodeListItem ) ; nil != li && nil != li . ListData {
n . ListData . Start = li . ListData . Num
}
}
2025-05-15 09:28:40 +08:00
} else if n . IsTextMarkType ( "code" ) {
if nil != n . Next && ast . NodeText == n . Next . Type {
// 行级代码导出 word 之后会有多余的零宽空格 https://github.com/siyuan-note/siyuan/issues/14825
n . Next . Tokens = bytes . TrimPrefix ( n . Tokens , [ ] byte ( editor . Zwsp ) )
}
2022-07-29 23:37:04 +08:00
}
return ast . WalkContinue
} )
2022-09-01 15:16:17 +08:00
if docx {
renderer := render . NewProtyleExportDocxRenderer ( tree , luteEngine . RenderOptions )
output := renderer . Render ( )
dom = gulu . Str . FromBytes ( output )
} else {
dom = luteEngine . ProtylePreview ( tree , luteEngine . RenderOptions )
}
2022-05-26 15:18:53 +08:00
return
}
2023-10-25 09:30:03 +08:00
func ExportHTML ( id , savePath string , pdf , image , keepFold , merge bool ) ( name , dom string , node * ast . Node ) {
2022-11-23 17:00:51 +08:00
savePath = strings . TrimSpace ( savePath )
2023-02-15 15:26:55 +08:00
bt := treenode . GetBlockTree ( id )
if nil == bt {
return
}
2023-02-15 17:59:57 +08:00
tree := prepareExportTree ( bt )
2023-10-25 09:30:03 +08:00
node = treenode . GetNodeInTree ( tree , id )
2022-12-11 11:42:37 +08:00
if merge {
var mergeErr error
tree , mergeErr = mergeSubDocs ( tree )
if nil != mergeErr {
logging . LogErrorf ( "merge sub docs failed: %s" , mergeErr )
return
}
}
2024-11-30 22:01:34 +08:00
blockRefMode := Conf . Export . BlockRefMode
2022-05-26 15:18:53 +08:00
var headings [ ] * ast . Node
if pdf { // 导出 PDF 需要标记目录书签
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if entering && ast . NodeHeading == n . Type && ! n . ParentIs ( ast . NodeBlockquote ) {
headings = append ( headings , n )
return ast . WalkSkipChildren
}
return ast . WalkContinue
} )
for _ , h := range headings {
link := & ast . Node { Type : ast . NodeLink }
link . AppendChild ( & ast . Node { Type : ast . NodeOpenBracket } )
link . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( " " ) } )
link . AppendChild ( & ast . Node { Type : ast . NodeCloseBracket } )
link . AppendChild ( & ast . Node { Type : ast . NodeOpenParen } )
2024-11-28 23:04:35 +08:00
link . AppendChild ( & ast . Node { Type : ast . NodeLinkDest , Tokens : [ ] byte ( PdfOutlineScheme + "://" + h . ID ) } )
2022-05-26 15:18:53 +08:00
link . AppendChild ( & ast . Node { Type : ast . NodeCloseParen } )
h . PrependChild ( link )
}
}
2024-08-11 09:33:43 +08:00
tree = exportTree ( tree , true , keepFold , true ,
2024-11-30 22:01:34 +08:00
blockRefMode , Conf . Export . BlockEmbedMode , Conf . Export . FileAnnotationRefMode ,
2023-01-02 23:26:01 +08:00
Conf . Export . TagOpenMarker , Conf . Export . TagCloseMarker ,
Conf . Export . BlockRefTextLeft , Conf . Export . BlockRefTextRight ,
2025-04-20 17:27:14 +08:00
Conf . Export . AddTitle , Conf . Export . InlineMemo , true , true , & map [ string ] * parse . Tree { } )
2022-09-08 11:33:44 +08:00
name = path . Base ( tree . HPath )
2022-08-11 23:52:50 +08:00
name = util . FilterFileName ( name ) // 导出 PDF、HTML 和 Word 时未移除不支持的文件名符号 https://github.com/siyuan-note/siyuan/issues/5614
2022-05-26 15:18:53 +08:00
2022-09-08 10:20:06 +08:00
if "" != savePath {
2024-09-04 04:40:50 +03:00
if err := os . MkdirAll ( savePath , 0755 ) ; err != nil {
2022-09-08 10:20:06 +08:00
logging . LogErrorf ( "mkdir [%s] failed: %s" , savePath , err )
return
2022-05-26 15:18:53 +08:00
}
2022-09-08 10:20:06 +08:00
assets := assetsLinkDestsInTree ( tree )
for _ , asset := range assets {
2022-09-19 16:53:22 +08:00
if strings . Contains ( asset , "?" ) {
asset = asset [ : strings . LastIndex ( asset , "?" ) ]
}
2022-09-08 10:20:06 +08:00
srcAbsPath , err := GetAssetAbsPath ( asset )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-09-08 10:20:06 +08:00
logging . LogWarnf ( "resolve path of asset [%s] failed: %s" , asset , err )
continue
}
targetAbsPath := filepath . Join ( savePath , asset )
2024-09-04 04:40:50 +03:00
if err = filelock . Copy ( srcAbsPath , targetAbsPath ) ; err != nil {
2022-09-08 10:20:06 +08:00
logging . LogWarnf ( "copy asset from [%s] to [%s] failed: %s" , srcAbsPath , targetAbsPath , err )
}
2022-05-26 15:18:53 +08:00
}
}
2022-09-08 10:20:06 +08:00
if ! pdf && "" != savePath { // 导出 HTML 需要复制静态资源
2024-12-23 16:14:16 +08:00
srcs := [ ] string { "stage/build/export" , "stage/protyle" }
2022-05-26 15:18:53 +08:00
for _ , src := range srcs {
from := filepath . Join ( util . WorkingDir , src )
to := filepath . Join ( savePath , src )
2024-09-04 04:40:50 +03:00
if err := filelock . Copy ( from , to ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "copy stage from [%s] to [%s] failed: %s" , from , savePath , err )
2022-05-26 15:18:53 +08:00
return
}
}
theme := Conf . Appearance . ThemeLight
if 1 == Conf . Appearance . Mode {
theme = Conf . Appearance . ThemeDark
}
srcs = [ ] string { "icons" , "themes/" + theme }
2023-09-13 08:54:29 +08:00
appearancePath := util . AppearancePath
if util . IsSymlinkPath ( util . AppearancePath ) {
// Support for symlinked theme folder when exporting HTML https://github.com/siyuan-note/siyuan/issues/9173
var readErr error
2023-09-13 08:58:25 +08:00
appearancePath , readErr = filepath . EvalSymlinks ( util . AppearancePath )
2023-09-13 08:54:29 +08:00
if nil != readErr {
logging . LogErrorf ( "readlink [%s] failed: %s" , util . AppearancePath , readErr )
return
}
}
2022-05-26 15:18:53 +08:00
for _ , src := range srcs {
2023-09-13 08:54:29 +08:00
from := filepath . Join ( appearancePath , src )
2022-05-26 15:18:53 +08:00
to := filepath . Join ( savePath , "appearance" , src )
2024-09-04 04:40:50 +03:00
if err := filelock . Copy ( from , to ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "copy appearance from [%s] to [%s] failed: %s" , from , savePath , err )
2022-05-26 15:18:53 +08:00
}
}
2022-07-28 23:47:55 +08:00
// 复制自定义表情图片
2022-07-29 23:37:04 +08:00
emojis := emojisInTree ( tree )
for _ , emoji := range emojis {
from := filepath . Join ( util . DataDir , emoji )
to := filepath . Join ( savePath , emoji )
2024-09-04 04:40:50 +03:00
if err := filelock . Copy ( from , to ) ; err != nil {
2024-12-19 23:51:12 +08:00
logging . LogErrorf ( "copy emojis from [%s] to [%s] failed: %s" , from , to , err )
2022-07-29 23:37:04 +08:00
}
2022-07-28 23:47:55 +08:00
}
2022-05-26 15:18:53 +08:00
}
if pdf {
processIFrame ( tree )
}
2024-12-02 11:50:32 +08:00
luteEngine := NewLute ( )
2022-05-26 15:18:53 +08:00
luteEngine . SetFootnotes ( true )
luteEngine . RenderOptions . ProtyleContenteditable = false
2022-09-17 16:32:05 +08:00
luteEngine . SetProtyleMarkNetImg ( false )
2024-05-20 11:47:58 +08:00
2022-10-17 09:34:27 +08:00
// 不进行安全过滤,因为导出时需要保留所有的 HTML 标签
// 使用属性 `data-export-html` 导出时 `<style></style>` 标签丢失 https://github.com/siyuan-note/siyuan/issues/6228
luteEngine . SetSanitize ( false )
2024-05-20 11:47:58 +08:00
2022-09-15 23:06:53 +08:00
renderer := render . NewProtyleExportRenderer ( tree , luteEngine . RenderOptions )
2022-05-26 15:18:53 +08:00
dom = gulu . Str . FromBytes ( renderer . Render ( ) )
return
}
2023-02-15 17:59:57 +08:00
func prepareExportTree ( bt * treenode . BlockTree ) ( ret * parse . Tree ) {
luteEngine := NewLute ( )
ret , _ = filesys . LoadTree ( bt . BoxID , bt . Path , luteEngine )
if "d" != bt . Type {
node := treenode . GetNodeInTree ( ret , bt . ID )
nodes := [ ] * ast . Node { node }
if "h" == bt . Type {
children := treenode . HeadingChildren ( node )
for _ , child := range children {
nodes = append ( nodes , child )
}
}
2024-01-15 22:15:34 +08:00
oldRoot := ret . Root
2023-02-15 17:59:57 +08:00
ret = parse . Parse ( "" , [ ] byte ( "" ) , luteEngine . ParseOptions )
first := ret . Root . FirstChild
2024-03-19 10:58:16 +08:00
for _ , n := range nodes {
first . InsertBefore ( n )
2023-02-15 17:59:57 +08:00
}
2024-01-15 22:15:34 +08:00
ret . Root . KramdownIAL = oldRoot . KramdownIAL
2023-02-15 17:59:57 +08:00
}
2023-04-01 16:52:35 +08:00
ret . Path = bt . Path
2023-02-15 17:59:57 +08:00
ret . HPath = bt . HPath
2023-04-01 16:52:35 +08:00
ret . Box = bt . BoxID
2023-05-12 17:02:31 +08:00
ret . ID = bt . RootID
2023-02-15 17:59:57 +08:00
return
}
2022-05-26 15:18:53 +08:00
func processIFrame ( tree * parse . Tree ) {
// 导出 PDF/Word 时 IFrame 块使用超链接 https://github.com/siyuan-note/siyuan/issues/4035
var unlinks [ ] * ast . Node
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if ast . NodeIFrame == n . Type {
index := bytes . Index ( n . Tokens , [ ] byte ( "src=\"" ) )
if 0 > index {
n . InsertBefore ( & ast . Node { Type : ast . NodeText , Tokens : n . Tokens } )
} else {
src := n . Tokens [ index + len ( "src=\"" ) : ]
src = src [ : bytes . Index ( src , [ ] byte ( "\"" ) ) ]
src = html . UnescapeHTML ( src )
link := & ast . Node { Type : ast . NodeLink }
link . AppendChild ( & ast . Node { Type : ast . NodeOpenBracket } )
link . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : src } )
link . AppendChild ( & ast . Node { Type : ast . NodeCloseBracket } )
link . AppendChild ( & ast . Node { Type : ast . NodeOpenParen } )
link . AppendChild ( & ast . Node { Type : ast . NodeLinkDest , Tokens : src } )
link . AppendChild ( & ast . Node { Type : ast . NodeCloseParen } )
n . InsertBefore ( link )
}
unlinks = append ( unlinks , n )
}
return ast . WalkContinue
} )
for _ , n := range unlinks {
n . Unlink ( )
}
}
2023-12-27 11:23:37 +08:00
func ProcessPDF ( id , p string , merge , removeAssets , watermark bool ) ( err error ) {
2024-03-10 23:27:13 +08:00
tree , _ := LoadTreeByBlockID ( id )
2022-05-26 15:18:53 +08:00
if nil == tree {
return
}
2023-02-23 13:39:17 +08:00
2022-12-11 12:17:43 +08:00
if merge {
var mergeErr error
tree , mergeErr = mergeSubDocs ( tree )
if nil != mergeErr {
logging . LogErrorf ( "merge sub docs failed: %s" , mergeErr )
return
}
}
2022-05-26 15:18:53 +08:00
var headings [ ] * ast . Node
2023-04-12 12:20:00 +08:00
assetDests := assetsLinkDestsInTree ( tree )
2022-05-26 15:18:53 +08:00
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
2023-02-20 15:45:11 +08:00
if ! entering {
return ast . WalkContinue
}
if ast . NodeHeading == n . Type && ! n . ParentIs ( ast . NodeBlockquote ) {
2022-05-26 15:18:53 +08:00
headings = append ( headings , n )
return ast . WalkSkipChildren
}
return ast . WalkContinue
} )
2024-11-28 20:21:40 +08:00
api . DisableConfigDir ( )
2023-12-27 11:23:37 +08:00
font . UserFontDir = filepath . Join ( util . HomeDir , ".config" , "siyuan" , "fonts" )
if mkdirErr := os . MkdirAll ( font . UserFontDir , 0755 ) ; nil != mkdirErr {
logging . LogErrorf ( "mkdir [%s] failed: %s" , font . UserFontDir , mkdirErr )
return
}
2025-04-24 20:48:16 +08:00
if loadErr := api . LoadUserFonts ( ) ; nil != loadErr {
logging . LogErrorf ( "load user fonts failed: %s" , loadErr )
}
2023-02-23 13:39:17 +08:00
pdfCtx , ctxErr := api . ReadContextFile ( p )
if nil != ctxErr {
logging . LogErrorf ( "read pdf context failed: %s" , ctxErr )
return
}
processPDFBookmarks ( pdfCtx , headings )
processPDFLinkEmbedAssets ( pdfCtx , assetDests , removeAssets )
2023-12-27 11:23:37 +08:00
processPDFWatermark ( pdfCtx , watermark )
2023-02-23 13:39:17 +08:00
2024-11-28 20:21:40 +08:00
pdfcpuVer := model . VersionStr
2024-11-28 20:49:16 +08:00
model . VersionStr = "SiYuan v" + util . Ver + " (pdfcpu " + pdfcpuVer + ")"
2023-02-23 13:39:17 +08:00
if writeErr := api . WriteContextFile ( pdfCtx , p ) ; nil != writeErr {
logging . LogErrorf ( "write pdf context failed: %s" , writeErr )
return
}
return
}
2022-05-26 15:18:53 +08:00
2024-11-28 20:21:40 +08:00
func processPDFWatermark ( pdfCtx * model . Context , watermark bool ) {
2023-12-24 12:16:27 +08:00
// Support adding the watermark on export PDF https://github.com/siyuan-note/siyuan/issues/9961
// https://pdfcpu.io/core/watermark
2023-12-27 11:23:37 +08:00
if ! watermark {
return
}
2023-12-27 09:38:05 +08:00
str := Conf . Export . PDFWatermarkStr
if "" == str {
2023-12-24 12:16:27 +08:00
return
}
2023-12-27 09:38:05 +08:00
if ! IsPaidUser ( ) {
2023-12-24 12:16:27 +08:00
return
}
2023-12-27 09:38:05 +08:00
mode := "text"
if gulu . File . IsExist ( str ) {
if ".pdf" == strings . ToLower ( filepath . Ext ( str ) ) {
mode = "pdf"
} else {
mode = "image"
}
}
2023-12-27 11:43:21 +08:00
desc := Conf . Export . PDFWatermarkDesc
if "text" == mode && util . ContainsCJK ( str ) {
// 中日韩文本水印需要安装字体文件
descParts := strings . Split ( desc , "," )
m := map [ string ] string { }
for _ , descPart := range descParts {
kv := strings . Split ( descPart , ":" )
if 2 != len ( kv ) {
continue
}
m [ kv [ 0 ] ] = kv [ 1 ]
}
2025-04-24 20:48:16 +08:00
useDefaultFont := true
if "" != m [ "fontname" ] {
listFonts , e := api . ListFonts ( )
var builtInFontNames [ ] string
if nil != e {
logging . LogInfof ( "listFont failed: %s" , e )
} else {
for _ , f := range listFonts {
if strings . Contains ( f , "(" ) {
f = f [ : strings . Index ( f , "(" ) ]
}
f = strings . TrimSpace ( f )
if strings . Contains ( f , ":" ) || "" == f || strings . Contains ( f , "Corefonts" ) || strings . Contains ( f , "Userfonts" ) {
continue
}
builtInFontNames = append ( builtInFontNames , f )
}
for _ , font := range builtInFontNames {
if font == m [ "fontname" ] {
useDefaultFont = false
break
}
}
}
}
if useDefaultFont {
m [ "fontname" ] = "LXGWWenKaiLite-Regular"
fontPath := filepath . Join ( util . AppearancePath , "fonts" , "LxgwWenKai-Lite-1.501" , "LXGWWenKaiLite-Regular.ttf" )
err := api . InstallFonts ( [ ] string { fontPath } )
if err != nil {
logging . LogErrorf ( "install font [%s] failed: %s" , fontPath , err )
}
}
2023-12-27 11:43:21 +08:00
descBuilder := bytes . Buffer { }
for k , v := range m {
descBuilder . WriteString ( k )
descBuilder . WriteString ( ":" )
descBuilder . WriteString ( v )
descBuilder . WriteString ( "," )
}
desc = descBuilder . String ( )
desc = desc [ : len ( desc ) - 1 ]
}
2023-12-27 09:38:05 +08:00
logging . LogInfof ( "add PDF watermark [mode=%s, str=%s, desc=%s]" , mode , str , desc )
2024-11-28 20:21:40 +08:00
var wm * model . Watermark
2023-12-27 11:43:21 +08:00
var err error
2023-12-24 12:16:27 +08:00
switch mode {
case "text" :
2024-11-28 20:21:40 +08:00
wm , err = pdfcpu . ParseTextWatermarkDetails ( str , desc , false , types . POINTS )
2023-12-24 12:16:27 +08:00
case "image" :
2024-11-28 20:21:40 +08:00
wm , err = pdfcpu . ParseImageWatermarkDetails ( str , desc , false , types . POINTS )
2023-12-24 12:16:27 +08:00
case "pdf" :
2024-11-28 20:21:40 +08:00
wm , err = pdfcpu . ParsePDFWatermarkDetails ( str , desc , false , types . POINTS )
2023-12-24 12:16:27 +08:00
}
2024-09-04 04:40:50 +03:00
if err != nil {
2023-12-24 12:16:27 +08:00
logging . LogErrorf ( "parse watermark failed: %s" , err )
2024-11-04 12:36:03 +08:00
util . PushErrMsg ( err . Error ( ) , 7000 )
2023-12-24 12:16:27 +08:00
return
}
2024-04-03 10:24:51 +08:00
wm . OnTop = true // Export PDF and add watermarks no longer covered by images https://github.com/siyuan-note/siyuan/issues/10818
2024-11-28 20:21:40 +08:00
err = pdfcpu . AddWatermarks ( pdfCtx , nil , wm )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-12-24 12:16:27 +08:00
logging . LogErrorf ( "add watermark failed: %s" , err )
return
}
}
2024-11-28 20:21:40 +08:00
func processPDFBookmarks ( pdfCtx * model . Context , headings [ ] * ast . Node ) {
links , err := PdfListToCLinks ( pdfCtx )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-02-23 13:39:17 +08:00
return
}
sort . Slice ( links , func ( i , j int ) bool {
return links [ i ] . Page < links [ j ] . Page
} )
2024-12-01 11:49:34 +08:00
titles := map [ string ] bool { }
2024-11-28 20:49:16 +08:00
bms := map [ string ] * pdfcpu . Bookmark { }
2023-02-23 13:39:17 +08:00
for _ , link := range links {
linkID := link . URI [ strings . LastIndex ( link . URI , "/" ) + 1 : ]
b := sql . GetBlock ( linkID )
if nil == b {
logging . LogWarnf ( "pdf outline block [%s] not found" , linkID )
continue
}
title := b . Content
title , _ = url . QueryUnescape ( title )
2024-12-01 11:49:34 +08:00
for {
if _ , ok := titles [ title ] ; ok {
title += "\x01"
} else {
titles [ title ] = true
break
}
}
2024-11-28 20:49:16 +08:00
bm := & pdfcpu . Bookmark {
2023-02-23 13:39:17 +08:00
Title : title ,
PageFrom : link . Page ,
AbsPos : link . Rect . UR . Y ,
}
bms [ linkID ] = bm
}
if 1 > len ( bms ) {
return
}
2024-11-28 20:49:16 +08:00
var topBms [ ] * pdfcpu . Bookmark
2023-02-23 13:39:17 +08:00
stack := linkedliststack . New ( )
for _ , h := range headings {
L :
for ; ; stack . Pop ( ) {
cur , ok := stack . Peek ( )
if ! ok {
2024-11-28 20:21:40 +08:00
bm , ok := bms [ h . ID ]
if ! ok {
2023-02-20 15:45:11 +08:00
break L
}
2023-02-23 13:39:17 +08:00
bm . Level = h . HeadingLevel
stack . Push ( bm )
topBms = append ( topBms , bm )
break L
2022-05-26 15:18:53 +08:00
}
2024-11-28 20:49:16 +08:00
tip := cur . ( * pdfcpu . Bookmark )
2023-02-23 13:39:17 +08:00
if tip . Level < h . HeadingLevel {
bm := bms [ h . ID ]
bm . Level = h . HeadingLevel
2024-11-28 20:49:16 +08:00
bm . Parent = tip
2024-11-28 20:21:40 +08:00
tip . Kids = append ( tip . Kids , bm )
2023-02-23 13:39:17 +08:00
stack . Push ( bm )
break L
}
2023-02-20 15:45:11 +08:00
}
2023-02-23 13:39:17 +08:00
}
2023-02-20 15:45:11 +08:00
2024-11-28 20:21:40 +08:00
err = pdfcpu . AddBookmarks ( pdfCtx , topBms , true )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-02-23 13:39:17 +08:00
logging . LogErrorf ( "add bookmark failed: %s" , err )
return
2022-05-26 15:18:53 +08:00
}
2023-02-23 13:39:17 +08:00
}
2023-02-20 15:45:11 +08:00
2023-02-23 13:39:17 +08:00
// processPDFLinkEmbedAssets 处理资源文件超链接,根据 removeAssets 参数决定是否将资源文件嵌入到 PDF 中。
// 导出 PDF 时支持将资源文件作为附件嵌入 https://github.com/siyuan-note/siyuan/issues/7414
2024-11-28 20:21:40 +08:00
func processPDFLinkEmbedAssets ( pdfCtx * model . Context , assetDests [ ] string , removeAssets bool ) {
2023-02-22 21:11:58 +08:00
var assetAbsPaths [ ] string
for _ , dest := range assetDests {
2023-02-23 13:39:17 +08:00
if absPath , _ := GetAssetAbsPath ( dest ) ; "" != absPath {
2023-02-22 21:11:58 +08:00
assetAbsPaths = append ( assetAbsPaths , absPath )
}
}
2023-02-23 13:39:17 +08:00
if 1 > len ( assetAbsPaths ) {
2023-02-23 11:18:05 +08:00
return
}
2024-11-28 20:21:40 +08:00
assetLinks , otherLinks , listErr := PdfListLinks ( pdfCtx )
2023-02-23 13:39:17 +08:00
if nil != listErr {
logging . LogErrorf ( "list asset links failed: %s" , listErr )
return
}
2024-11-28 23:04:35 +08:00
if 1 > len ( assetLinks ) {
return
}
2024-11-28 20:21:40 +08:00
if _ , removeErr := pdfcpu . RemoveAnnotations ( pdfCtx , nil , nil , nil , false ) ; nil != removeErr {
2023-02-23 13:39:17 +08:00
logging . LogWarnf ( "remove annotations failed: %s" , removeErr )
}
2023-02-22 21:11:58 +08:00
2024-11-28 20:21:40 +08:00
linkMap := map [ int ] [ ] model . AnnotationRenderer { }
2023-02-23 13:39:17 +08:00
for _ , link := range otherLinks {
link . URI , _ = url . PathUnescape ( link . URI )
if 1 > len ( linkMap [ link . Page ] ) {
2024-11-28 20:21:40 +08:00
linkMap [ link . Page ] = [ ] model . AnnotationRenderer { link }
2023-02-23 13:39:17 +08:00
} else {
linkMap [ link . Page ] = append ( linkMap [ link . Page ] , link )
2023-02-22 21:11:58 +08:00
}
2023-02-23 13:39:17 +08:00
}
2024-11-28 20:21:40 +08:00
attachmentMap := map [ int ] [ ] * types . IndirectRef { }
now := types . StringLiteral ( types . DateString ( time . Now ( ) ) )
2023-02-23 13:39:17 +08:00
for _ , link := range assetLinks {
2023-02-24 10:21:46 +08:00
link . URI = strings . ReplaceAll ( link . URI , "http://" + util . LocalHost + ":" + util . ServerPort + "/export/temp/" , "" )
2023-09-04 16:19:19 +08:00
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
2023-02-23 13:39:17 +08:00
link . URI , _ = url . PathUnescape ( link . URI )
2023-04-12 12:20:00 +08:00
if idx := strings . Index ( link . URI , "?" ) ; 0 < idx {
link . URI = link . URI [ : idx ]
}
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
if ! removeAssets {
// 不移除资源文件夹的话将超链接指向资源文件夹
2023-02-23 11:18:05 +08:00
if 1 > len ( linkMap [ link . Page ] ) {
2024-11-28 20:21:40 +08:00
linkMap [ link . Page ] = [ ] model . AnnotationRenderer { link }
2023-02-23 11:18:05 +08:00
} else {
linkMap [ link . Page ] = append ( linkMap [ link . Page ] , link )
}
2023-02-23 13:39:17 +08:00
continue
}
2023-02-23 11:18:05 +08:00
2023-02-23 13:39:17 +08:00
// 移除资源文件夹的话使用内嵌附件
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
absPath , getErr := GetAssetAbsPath ( link . URI )
if nil != getErr {
continue
}
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
ir , newErr := pdfCtx . XRefTable . NewEmbeddedFileStreamDict ( absPath )
if nil != newErr {
logging . LogWarnf ( "new embedded file stream dict failed: %s" , newErr )
continue
}
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
fn := filepath . Base ( absPath )
2024-11-28 20:49:16 +08:00
fileSpecDict , newErr := pdfCtx . XRefTable . NewFileSpecDict ( fn , fn , "attached by SiYuan" , * ir )
2023-02-23 13:39:17 +08:00
if nil != newErr {
logging . LogWarnf ( "new file spec dict failed: %s" , newErr )
continue
}
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
ir , indErr := pdfCtx . XRefTable . IndRefForNewObject ( fileSpecDict )
if nil != indErr {
logging . LogWarnf ( "ind ref for new object failed: %s" , indErr )
continue
}
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
lx := link . Rect . LL . X + link . Rect . Width ( )
ly := link . Rect . LL . Y + link . Rect . Height ( ) / 2
2024-11-28 20:49:16 +08:00
w := link . Rect . Height ( ) / 2
h := link . Rect . Height ( ) / 2
2023-02-23 13:39:17 +08:00
2024-11-28 20:21:40 +08:00
d := types . Dict (
map [ string ] types . Object {
"Type" : types . Name ( "Annot" ) ,
"Subtype" : types . Name ( "FileAttachment" ) ,
"Contents" : types . StringLiteral ( "" ) ,
2024-11-28 20:49:16 +08:00
"Rect" : types . RectForWidthAndHeight ( lx , ly , w , h ) . Array ( ) ,
2023-02-23 13:39:17 +08:00
"P" : link . P ,
"M" : now ,
2024-11-28 20:21:40 +08:00
"F" : types . Integer ( 0 ) ,
"Border" : types . NewIntegerArray ( 0 , 0 , 1 ) ,
"C" : types . NewNumberArray ( 0.5 , 0.0 , 0.5 ) ,
"CA" : types . Float ( 0.95 ) ,
2023-02-23 13:39:17 +08:00
"CreationDate" : now ,
2024-11-28 20:21:40 +08:00
"Name" : types . Name ( "FileAttachment" ) ,
2023-02-23 13:39:17 +08:00
"FS" : * ir ,
2024-11-28 20:21:40 +08:00
"NM" : types . StringLiteral ( "" ) ,
2023-02-23 13:39:17 +08:00
} ,
)
ann , indErr := pdfCtx . XRefTable . IndRefForNewObject ( d )
if nil != indErr {
logging . LogWarnf ( "ind ref for new object failed: %s" , indErr )
continue
}
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
pageDictIndRef , pageErr := pdfCtx . PageDictIndRef ( link . Page )
if nil != pageErr {
logging . LogWarnf ( "page dict ind ref failed: %s" , pageErr )
continue
}
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
d , defErr := pdfCtx . DereferenceDict ( * pageDictIndRef )
if nil != defErr {
logging . LogWarnf ( "dereference dict failed: %s" , defErr )
continue
2023-02-23 11:18:05 +08:00
}
2023-02-23 13:39:17 +08:00
if 1 > len ( attachmentMap [ link . Page ] ) {
2024-11-28 20:21:40 +08:00
attachmentMap [ link . Page ] = [ ] * types . IndirectRef { ann }
2023-02-23 13:39:17 +08:00
} else {
attachmentMap [ link . Page ] = append ( attachmentMap [ link . Page ] , ann )
2023-02-22 21:11:58 +08:00
}
2023-02-23 13:39:17 +08:00
}
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
if 0 < len ( linkMap ) {
2024-11-28 20:21:40 +08:00
if _ , addErr := pdfcpu . AddAnnotationsMap ( pdfCtx , linkMap , false ) ; nil != addErr {
2023-02-23 13:39:17 +08:00
logging . LogErrorf ( "add annotations map failed: %s" , addErr )
}
}
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
// 添加附件注解指向内嵌的附件
for page , anns := range attachmentMap {
pageDictIndRef , pageErr := pdfCtx . PageDictIndRef ( page )
if nil != pageErr {
logging . LogWarnf ( "page dict ind ref failed: %s" , pageErr )
continue
}
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
pageDict , defErr := pdfCtx . DereferenceDict ( * pageDictIndRef )
if nil != defErr {
logging . LogWarnf ( "dereference dict failed: %s" , defErr )
continue
}
2023-02-22 21:11:58 +08:00
2024-11-28 20:21:40 +08:00
array := types . Array { }
2023-02-23 13:39:17 +08:00
for _ , ann := range anns {
array = append ( array , * ann )
}
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
obj , found := pageDict . Find ( "Annots" )
if ! found {
pageDict . Insert ( "Annots" , array )
pdfCtx . EnsureVersionForWriting ( )
continue
}
2023-02-22 21:11:58 +08:00
2024-11-28 20:21:40 +08:00
ir , ok := obj . ( types . IndirectRef )
2023-02-23 13:39:17 +08:00
if ! ok {
2024-11-28 20:21:40 +08:00
pageDict . Update ( "Annots" , append ( obj . ( types . Array ) , array ... ) )
2023-02-23 13:39:17 +08:00
pdfCtx . EnsureVersionForWriting ( )
continue
}
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
// Annots array is an IndirectReference.
2023-02-22 21:11:58 +08:00
2023-02-23 13:39:17 +08:00
o , err := pdfCtx . Dereference ( ir )
if err != nil || o == nil {
continue
2023-02-22 21:11:58 +08:00
}
2024-11-28 20:21:40 +08:00
annots , _ := o . ( types . Array )
2023-02-23 13:39:17 +08:00
entry , ok := pdfCtx . FindTableEntryForIndRef ( & ir )
if ! ok {
continue
}
entry . Object = append ( annots , array ... )
pdfCtx . EnsureVersionForWriting ( )
2023-02-22 21:11:58 +08:00
}
2022-05-26 15:18:53 +08:00
}
2025-05-18 10:53:40 +08:00
func ExportStdMarkdown ( id string , assetsDestSpace2Underscore bool ) string {
2024-03-10 23:27:13 +08:00
tree , err := LoadTreeByBlockID ( id )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-01-02 22:22:11 +08:00
logging . LogErrorf ( "load tree by block id [%s] failed: %s" , id , err )
return ""
}
cloudAssetsBase := ""
2022-05-26 15:18:53 +08:00
if IsSubscriber ( ) {
2023-12-08 21:46:46 +08:00
cloudAssetsBase = util . GetCloudAssetsServer ( ) + Conf . GetUser ( ) . UserId + "/"
2022-05-26 15:18:53 +08:00
}
2024-12-09 17:46:41 +08:00
var defBlockIDs [ ] string
if 4 == Conf . Export . BlockRefMode { // 脚注+锚点哈希
// 导出锚点哈希,这里先记录下所有定义块的 ID
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
var defID string
if treenode . IsBlockLink ( n ) {
defID = strings . TrimPrefix ( n . TextMarkAHref , "siyuan://blocks/" )
} else if treenode . IsBlockRef ( n ) {
defID , _ , _ = treenode . GetBlockRef ( n )
}
if "" != defID {
if defBt := treenode . GetBlockTree ( defID ) ; nil != defBt {
defBlockIDs = append ( defBlockIDs , defID )
defBlockIDs = gulu . Str . RemoveDuplicatedElem ( defBlockIDs )
}
}
return ast . WalkContinue
} )
}
defBlockIDs = gulu . Str . RemoveDuplicatedElem ( defBlockIDs )
2025-05-18 10:53:40 +08:00
return exportMarkdownContent0 ( tree , cloudAssetsBase , assetsDestSpace2Underscore ,
2024-12-18 01:06:58 +08:00
".md" , Conf . Export . BlockRefMode , Conf . Export . BlockEmbedMode , Conf . Export . FileAnnotationRefMode ,
2023-01-02 23:26:01 +08:00
Conf . Export . TagOpenMarker , Conf . Export . TagCloseMarker ,
Conf . Export . BlockRefTextLeft , Conf . Export . BlockRefTextRight ,
2025-04-20 17:27:14 +08:00
Conf . Export . AddTitle , Conf . Export . InlineMemo , defBlockIDs , true , & map [ string ] * parse . Tree { } )
2022-05-26 15:18:53 +08:00
}
2024-12-18 01:06:58 +08:00
func ExportPandocConvertZip ( ids [ ] string , pandocTo , ext string ) ( name , zipPath string ) {
2024-11-09 14:39:27 +08:00
block := treenode . GetBlockTree ( ids [ 0 ] )
box := Conf . Box ( block . BoxID )
baseFolderName := path . Base ( block . HPath )
if "." == baseFolderName {
baseFolderName = path . Base ( block . Path )
}
2024-11-18 10:49:22 +08:00
var docPaths [ ] string
2024-11-09 14:39:27 +08:00
bts := treenode . GetBlockTrees ( ids )
for _ , bt := range bts {
2024-11-18 10:49:22 +08:00
docPaths = append ( docPaths , bt . Path )
2024-11-09 14:39:27 +08:00
docFiles := box . ListFiles ( strings . TrimSuffix ( bt . Path , ".sy" ) )
for _ , docFile := range docFiles {
docPaths = append ( docPaths , docFile . path )
}
}
2024-12-18 01:06:58 +08:00
defBlockIDs , trees , docPaths := prepareExportTrees ( docPaths )
zipPath = exportPandocConvertZip ( baseFolderName , docPaths , defBlockIDs , "gfm+footnotes+hard_line_breaks" , pandocTo , ext , trees )
2024-11-29 08:41:43 +08:00
name = util . GetTreeID ( block . Path )
2022-05-26 15:18:53 +08:00
return
}
2024-12-18 17:17:24 +08:00
func ExportNotebookMarkdown ( boxID string ) ( zipPath string ) {
2025-01-01 11:02:16 +08:00
util . PushEndlessProgress ( Conf . Language ( 65 ) )
defer util . ClearPushProgress ( 100 )
2022-05-26 15:18:53 +08:00
box := Conf . Box ( boxID )
2025-01-01 11:02:16 +08:00
if nil == box {
logging . LogErrorf ( "not found box [%s]" , boxID )
return
}
2022-05-26 15:18:53 +08:00
var docPaths [ ] string
2025-01-01 11:02:16 +08:00
docFiles := box . ListFiles ( "/" )
2022-05-26 15:18:53 +08:00
for _ , docFile := range docFiles {
docPaths = append ( docPaths , docFile . path )
}
2024-12-18 01:06:58 +08:00
defBlockIDs , trees , docPaths := prepareExportTrees ( docPaths )
2024-12-18 17:17:24 +08:00
zipPath = exportPandocConvertZip ( box . Name , docPaths , defBlockIDs , "" , "" , ".md" , trees )
2022-05-26 15:18:53 +08:00
return
}
2022-12-17 11:53:11 +08:00
func yfm ( docIAL map [ string ] string ) string {
// 导出 Markdown 文件时开头附上一些元数据 https://github.com/siyuan-note/siyuan/issues/6880
2024-10-30 09:23:20 +08:00
2022-12-17 11:53:11 +08:00
buf := bytes . Buffer { }
buf . WriteString ( "---\n" )
var title , created , updated , tags string
for k , v := range docIAL {
if "id" == k {
createdTime , parseErr := time . Parse ( "20060102150405" , util . TimeFromID ( v ) )
if nil == parseErr {
created = createdTime . Format ( time . RFC3339 )
}
continue
}
if "title" == k {
title = v
continue
}
if "updated" == k {
updatedTime , parseErr := time . Parse ( "20060102150405" , v )
if nil == parseErr {
updated = updatedTime . Format ( time . RFC3339 )
}
continue
}
if "tags" == k {
tags = v
continue
}
}
if "" != title {
buf . WriteString ( "title: " )
buf . WriteString ( title )
buf . WriteString ( "\n" )
}
if "" == updated {
updated = time . Now ( ) . Format ( time . RFC3339 )
}
if "" == created {
created = updated
}
2022-12-22 10:24:28 +08:00
buf . WriteString ( "date: " )
2022-12-17 11:53:11 +08:00
buf . WriteString ( created )
buf . WriteString ( "\n" )
2022-12-22 10:24:28 +08:00
buf . WriteString ( "lastmod: " )
2022-12-17 11:53:11 +08:00
buf . WriteString ( updated )
buf . WriteString ( "\n" )
if "" != tags {
buf . WriteString ( "tags: [" )
buf . WriteString ( tags )
buf . WriteString ( "]\n" )
}
buf . WriteString ( "---\n\n" )
return buf . String ( )
}
2022-07-24 22:05:14 +08:00
func exportBoxSYZip ( boxID string ) ( zipPath string ) {
2024-06-12 12:03:31 +08:00
util . PushEndlessProgress ( Conf . Language ( 65 ) )
defer util . ClearPushProgress ( 100 )
2022-07-24 22:05:14 +08:00
box := Conf . Box ( boxID )
if nil == box {
logging . LogErrorf ( "not found box [%s]" , boxID )
return
}
baseFolderName := box . Name
var docPaths [ ] string
docFiles := box . ListFiles ( "/" )
for _ , docFile := range docFiles {
docPaths = append ( docPaths , docFile . path )
}
zipPath = exportSYZip ( boxID , "/" , baseFolderName , docPaths )
return
}
2022-05-26 15:18:53 +08:00
func exportSYZip ( boxID , rootDirPath , baseFolderName string , docPaths [ ] string ) ( zipPath string ) {
2024-06-19 09:38:02 +08:00
defer util . ClearPushProgress ( 100 )
2022-05-26 15:18:53 +08:00
dir , name := path . Split ( baseFolderName )
name = util . FilterFileName ( name )
if strings . HasSuffix ( name , ".." ) {
// 文档标题以 `..` 结尾时无法导出 Markdown https://github.com/siyuan-note/siyuan/issues/4698
// 似乎是 os.MkdirAll 的 bug, 以 .. 结尾的路径无法创建,所以这里加上 _ 结尾
name += "_"
}
baseFolderName = path . Join ( dir , name )
box := Conf . Box ( boxID )
exportFolder := filepath . Join ( util . TempDir , "export" , baseFolderName )
2024-09-04 04:40:50 +03:00
if err := os . MkdirAll ( exportFolder , 0755 ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "create export temp folder failed: %s" , err )
2022-05-26 15:18:53 +08:00
return
}
trees := map [ string ] * parse . Tree { }
refTrees := map [ string ] * parse . Tree { }
2024-10-19 11:43:53 +08:00
luteEngine := util . NewLute ( )
2024-09-30 10:53:05 +08:00
for i , p := range docPaths {
2024-11-04 11:40:03 +08:00
if ! strings . HasSuffix ( p , ".sy" ) {
continue
}
2024-10-19 11:43:53 +08:00
tree , err := filesys . LoadTree ( boxID , p , luteEngine )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-05-26 15:18:53 +08:00
continue
}
trees [ tree . ID ] = tree
2024-09-30 10:53:05 +08:00
2024-10-19 11:43:53 +08:00
util . PushEndlessProgress ( Conf . language ( 65 ) + " " + fmt . Sprintf ( Conf . language ( 70 ) , fmt . Sprintf ( "%d/%d %s" , i + 1 , len ( docPaths ) , tree . Root . IALAttr ( "title" ) ) ) )
}
count := 1
treeCache := map [ string ] * parse . Tree { }
for _ , tree := range trees {
util . PushEndlessProgress ( Conf . language ( 65 ) + " " + fmt . Sprintf ( Conf . language ( 70 ) , fmt . Sprintf ( "%d/%d %s" , count , len ( docPaths ) , tree . Root . IALAttr ( "title" ) ) ) )
2024-10-17 23:02:27 +08:00
refs := map [ string ] * parse . Tree { }
2024-12-18 01:06:58 +08:00
exportRefTrees ( tree , & [ ] string { } , & refs , & treeCache )
2022-05-26 15:18:53 +08:00
for refTreeID , refTree := range refs {
if nil == trees [ refTreeID ] {
refTrees [ refTreeID ] = refTree
}
}
2024-10-19 23:49:15 +08:00
count ++
2022-05-26 15:18:53 +08:00
}
2024-09-30 10:53:05 +08:00
util . PushEndlessProgress ( Conf . Language ( 65 ) )
2024-10-19 11:43:53 +08:00
count = 0
2024-09-30 10:53:05 +08:00
2022-05-26 15:18:53 +08:00
// 按文件夹结构复制选择的树
2024-10-01 23:19:04 +08:00
total := len ( trees ) + len ( refTrees )
2022-05-26 15:18:53 +08:00
for _ , tree := range trees {
readPath := filepath . Join ( util . DataDir , tree . Box , tree . Path )
2022-09-29 21:52:01 +08:00
data , readErr := filelock . ReadFile ( readPath )
2022-05-26 15:18:53 +08:00
if nil != readErr {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "read file [%s] failed: %s" , readPath , readErr )
2022-05-26 15:18:53 +08:00
continue
}
writePath := strings . TrimPrefix ( tree . Path , rootDirPath )
writePath = filepath . Join ( exportFolder , writePath )
writeFolder := filepath . Dir ( writePath )
if mkdirErr := os . MkdirAll ( writeFolder , 0755 ) ; nil != mkdirErr {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "create export temp folder [%s] failed: %s" , writeFolder , mkdirErr )
2022-05-26 15:18:53 +08:00
continue
}
if writeErr := os . WriteFile ( writePath , data , 0644 ) ; nil != writeErr {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "write export file [%s] failed: %s" , writePath , writeErr )
2022-05-26 15:18:53 +08:00
continue
}
2024-10-01 23:19:04 +08:00
count ++
2024-10-19 11:43:53 +08:00
util . PushEndlessProgress ( Conf . language ( 65 ) + " " + fmt . Sprintf ( Conf . Language ( 66 ) , fmt . Sprintf ( "%d/%d " , count , total ) + tree . HPath ) )
2022-05-26 15:18:53 +08:00
}
2024-10-19 23:49:15 +08:00
count = 0
2022-05-26 15:18:53 +08:00
// 引用树放在导出文件夹根路径下
for treeID , tree := range refTrees {
readPath := filepath . Join ( util . DataDir , tree . Box , tree . Path )
2022-09-29 21:52:01 +08:00
data , readErr := filelock . ReadFile ( readPath )
2022-05-26 15:18:53 +08:00
if nil != readErr {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "read file [%s] failed: %s" , readPath , readErr )
2022-05-26 15:18:53 +08:00
continue
}
writePath := strings . TrimPrefix ( tree . Path , rootDirPath )
writePath = filepath . Join ( exportFolder , treeID + ".sy" )
if writeErr := os . WriteFile ( writePath , data , 0644 ) ; nil != writeErr {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "write export file [%s] failed: %s" , writePath , writeErr )
2022-05-26 15:18:53 +08:00
continue
}
2024-10-01 23:19:04 +08:00
count ++
2024-10-19 11:43:53 +08:00
util . PushEndlessProgress ( Conf . language ( 65 ) + " " + fmt . Sprintf ( Conf . Language ( 66 ) , fmt . Sprintf ( "%d/%d " , count , total ) + tree . HPath ) )
2022-05-26 15:18:53 +08:00
}
// 将引用树合并到选择树中,以便后面一次性导出资源文件
for treeID , tree := range refTrees {
trees [ treeID ] = tree
}
// 导出引用的资源文件
2024-12-16 23:06:01 +08:00
assetPathMap , err := allAssetAbsPaths ( )
if nil != err {
logging . LogWarnf ( "get assets abs path failed: %s" , err )
return
}
2022-05-26 15:18:53 +08:00
copiedAssets := hashset . New ( )
for _ , tree := range trees {
var assets [ ] string
assets = append ( assets , assetsLinkDestsInTree ( tree ) ... )
2023-07-14 18:35:38 +08:00
titleImgPath := treenode . GetDocTitleImgPath ( tree . Root ) // Export .sy.zip doc title image is not exported https://github.com/siyuan-note/siyuan/issues/8748
if "" != titleImgPath {
2024-11-04 11:40:03 +08:00
if util . IsAssetLinkDest ( [ ] byte ( titleImgPath ) ) {
assets = append ( assets , titleImgPath )
}
2023-07-14 18:35:38 +08:00
}
2022-05-26 15:18:53 +08:00
for _ , asset := range assets {
2024-10-11 14:40:30 +08:00
util . PushEndlessProgress ( Conf . language ( 65 ) + " " + fmt . Sprintf ( Conf . language ( 70 ) , asset ) )
2022-05-26 15:18:53 +08:00
asset = string ( html . DecodeDestination ( [ ] byte ( asset ) ) )
if strings . Contains ( asset , "?" ) {
asset = asset [ : strings . LastIndex ( asset , "?" ) ]
}
if copiedAssets . Contains ( asset ) {
continue
}
2024-12-16 23:06:01 +08:00
srcPath := assetPathMap [ asset ]
if "" == srcPath {
logging . LogWarnf ( "get asset [%s] abs path failed" , asset )
2022-05-26 15:18:53 +08:00
continue
}
destPath := filepath . Join ( exportFolder , asset )
2024-12-16 23:06:01 +08:00
assetErr := filelock . Copy ( srcPath , destPath )
2022-05-26 15:18:53 +08:00
if nil != assetErr {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "copy asset from [%s] to [%s] failed: %s" , srcPath , destPath , assetErr )
2022-05-26 15:18:53 +08:00
continue
}
2023-03-31 10:36:50 +08:00
if ! gulu . File . IsDir ( srcPath ) && strings . HasSuffix ( strings . ToLower ( srcPath ) , ".pdf" ) {
sya := srcPath + ".sya"
2023-11-06 22:13:04 +08:00
if filelock . IsExist ( sya ) {
2023-03-31 10:36:50 +08:00
// Related PDF annotation information is not exported when exporting .sy.zip https://github.com/siyuan-note/siyuan/issues/7836
if syaErr := filelock . Copy ( sya , destPath + ".sya" ) ; nil != syaErr {
logging . LogErrorf ( "copy sya from [%s] to [%s] failed: %s" , sya , destPath + ".sya" , syaErr )
}
}
}
2022-05-26 15:18:53 +08:00
copiedAssets . Add ( asset )
}
2024-12-19 23:51:12 +08:00
// 复制自定义表情图片
emojis := emojisInTree ( tree )
for _ , emoji := range emojis {
from := filepath . Join ( util . DataDir , emoji )
to := filepath . Join ( exportFolder , emoji )
if copyErr := filelock . Copy ( from , to ) ; copyErr != nil {
logging . LogErrorf ( "copy emojis from [%s] to [%s] failed: %s" , from , to , copyErr )
}
}
2022-05-26 15:18:53 +08:00
}
2023-09-25 21:50:02 +08:00
// 导出数据库 Attribute View export https://github.com/siyuan-note/siyuan/issues/8710
2023-09-25 22:13:09 +08:00
exportStorageAvDir := filepath . Join ( exportFolder , "storage" , "av" )
2023-09-25 21:50:02 +08:00
for _ , tree := range trees {
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if ast . NodeAttributeView != n . Type {
return ast . WalkContinue
}
avID := n . AttributeViewID
2023-09-25 22:13:09 +08:00
avJSONPath := av . GetAttributeViewDataPath ( avID )
2023-11-06 22:13:04 +08:00
if ! filelock . IsExist ( avJSONPath ) {
2023-09-25 21:50:02 +08:00
return ast . WalkContinue
}
2023-09-25 22:13:09 +08:00
if copyErr := filelock . Copy ( avJSONPath , filepath . Join ( exportStorageAvDir , avID + ".json" ) ) ; nil != copyErr {
logging . LogErrorf ( "copy av json failed: %s" , copyErr )
}
2023-12-19 10:47:33 +08:00
attrView , err := av . ParseAttributeView ( avID )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-12-19 10:47:33 +08:00
logging . LogErrorf ( "parse attribute view [%s] failed: %s" , avID , err )
return ast . WalkContinue
}
for _ , keyValues := range attrView . KeyValues {
2023-12-31 17:49:38 +08:00
switch keyValues . Key . Type {
2024-01-01 17:38:06 +08:00
case av . KeyTypeMAsset : // 导出资源文件列 https://github.com/siyuan-note/siyuan/issues/9919
2023-12-31 17:49:38 +08:00
for _ , value := range keyValues . Values {
for _ , asset := range value . MAsset {
2024-11-04 11:40:03 +08:00
if ! util . IsAssetLinkDest ( [ ] byte ( asset . Content ) ) {
2023-12-31 17:49:38 +08:00
continue
}
destPath := filepath . Join ( exportFolder , asset . Content )
2024-12-16 23:06:01 +08:00
srcPath := assetPathMap [ asset . Content ]
if "" == srcPath {
logging . LogWarnf ( "get asset [%s] abs path failed" , asset . Content )
2023-12-31 17:49:38 +08:00
continue
}
if copyErr := filelock . Copy ( srcPath , destPath ) ; nil != copyErr {
logging . LogErrorf ( "copy asset failed: %s" , copyErr )
}
2023-12-19 10:47:33 +08:00
}
2023-12-31 17:49:38 +08:00
}
2023-12-19 10:47:33 +08:00
}
}
2024-01-01 17:38:06 +08:00
// 级联导出关联列关联的数据库
exportRelationAvs ( avID , exportStorageAvDir )
2023-09-25 21:50:02 +08:00
return ast . WalkContinue
} )
}
2023-10-08 16:35:06 +08:00
// 导出闪卡 Export related flashcard data when exporting .sy.zip https://github.com/siyuan-note/siyuan/issues/9372
exportStorageRiffDir := filepath . Join ( exportFolder , "storage" , "riff" )
2023-10-08 19:17:54 +08:00
deck , loadErr := riff . LoadDeck ( exportStorageRiffDir , builtinDeckID , Conf . Flashcard . RequestRetention , Conf . Flashcard . MaximumInterval , Conf . Flashcard . Weights )
if nil != loadErr {
logging . LogErrorf ( "load deck [%s] failed: %s" , name , loadErr )
} else {
for _ , tree := range trees {
cards := getTreeFlashcards ( tree . ID )
2023-10-08 16:35:06 +08:00
2023-10-08 19:17:54 +08:00
for _ , card := range cards {
deck . AddCard ( card . ID ( ) , card . BlockID ( ) )
}
2023-10-08 16:35:06 +08:00
}
2023-10-08 19:17:54 +08:00
if 0 < deck . CountCards ( ) {
if saveErr := deck . Save ( ) ; nil != saveErr {
logging . LogErrorf ( "save deck [%s] failed: %s" , name , saveErr )
}
2023-10-08 16:35:06 +08:00
}
}
2022-09-24 09:34:32 +08:00
// 导出自定义排序
sortPath := filepath . Join ( util . DataDir , box . ID , ".siyuan" , "sort.json" )
fullSortIDs := map [ string ] int { }
sortIDs := map [ string ] int { }
var sortData [ ] byte
var sortErr error
2023-11-06 22:13:04 +08:00
if filelock . IsExist ( sortPath ) {
2022-09-29 21:52:01 +08:00
sortData , sortErr = filelock . ReadFile ( sortPath )
2022-09-24 09:34:32 +08:00
if nil != sortErr {
logging . LogErrorf ( "read sort conf failed: %s" , sortErr )
}
if sortErr = gulu . JSON . UnmarshalJSON ( sortData , & fullSortIDs ) ; nil != sortErr {
logging . LogErrorf ( "unmarshal sort conf failed: %s" , sortErr )
}
if 0 < len ( fullSortIDs ) {
for _ , tree := range trees {
if v , ok := fullSortIDs [ tree . ID ] ; ok {
sortIDs [ tree . ID ] = v
}
}
}
if 0 < len ( sortIDs ) {
sortData , sortErr = gulu . JSON . MarshalJSON ( sortIDs )
if nil != sortErr {
logging . LogErrorf ( "marshal sort conf failed: %s" , sortErr )
}
if 0 < len ( sortData ) {
confDir := filepath . Join ( exportFolder , ".siyuan" )
if mkdirErr := os . MkdirAll ( confDir , 0755 ) ; nil != mkdirErr {
logging . LogErrorf ( "create export conf folder [%s] failed: %s" , confDir , mkdirErr )
} else {
sortPath = filepath . Join ( confDir , "sort.json" )
if writeErr := os . WriteFile ( sortPath , sortData , 0644 ) ; nil != writeErr {
logging . LogErrorf ( "write sort conf failed: %s" , writeErr )
}
}
}
}
}
2022-05-26 15:18:53 +08:00
zipPath = exportFolder + ".sy.zip"
zip , err := gulu . Zip . Create ( zipPath )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-05-10 22:53:23 +08:00
logging . LogErrorf ( "create export .sy.zip [%s] failed: %s" , exportFolder , err )
2022-05-26 15:18:53 +08:00
return ""
}
2024-06-12 12:03:31 +08:00
zipCallback := func ( filename string ) {
2025-01-01 11:02:16 +08:00
util . PushEndlessProgress ( Conf . language ( 65 ) + " " + fmt . Sprintf ( Conf . language ( 253 ) , filename ) )
2024-06-12 12:03:31 +08:00
}
2024-09-04 04:40:50 +03:00
if err = zip . AddDirectory ( baseFolderName , exportFolder , zipCallback ) ; err != nil {
2023-05-10 22:53:23 +08:00
logging . LogErrorf ( "create export .sy.zip [%s] failed: %s" , exportFolder , err )
2022-05-26 15:18:53 +08:00
return ""
}
2024-09-04 04:40:50 +03:00
if err = zip . Close ( ) ; err != nil {
2023-05-10 22:53:23 +08:00
logging . LogErrorf ( "close export .sy.zip failed: %s" , err )
2022-05-26 15:18:53 +08:00
}
os . RemoveAll ( exportFolder )
zipPath = "/export/" + url . PathEscape ( filepath . Base ( zipPath ) )
return
}
2024-01-01 17:38:06 +08:00
func exportRelationAvs ( avID , exportStorageAvDir string ) {
avIDs := hashset . New ( )
2024-01-01 17:44:21 +08:00
walkRelationAvs ( avID , avIDs )
2024-01-01 17:38:06 +08:00
for _ , v := range avIDs . Values ( ) {
relAvID := v . ( string )
relAvJSONPath := av . GetAttributeViewDataPath ( relAvID )
if ! filelock . IsExist ( relAvJSONPath ) {
continue
}
if copyErr := filelock . Copy ( relAvJSONPath , filepath . Join ( exportStorageAvDir , relAvID + ".json" ) ) ; nil != copyErr {
logging . LogErrorf ( "copy av json failed: %s" , copyErr )
}
}
}
2024-01-01 17:44:21 +08:00
func walkRelationAvs ( avID string , exportAvIDs * hashset . Set ) {
2024-01-01 17:38:06 +08:00
if exportAvIDs . Contains ( avID ) {
return
}
attrView , _ := av . ParseAttributeView ( avID )
if nil == attrView {
return
}
exportAvIDs . Add ( avID )
for _ , keyValues := range attrView . KeyValues {
switch keyValues . Key . Type {
case av . KeyTypeRelation : // 导出关联列
if nil == keyValues . Key . Relation {
break
}
2024-01-01 17:44:21 +08:00
walkRelationAvs ( keyValues . Key . Relation . AvID , exportAvIDs )
2024-01-01 17:38:06 +08:00
}
}
}
2025-02-09 12:20:18 +08:00
func ExportMarkdownContent ( id string , refMode , embedMode int , addYfm bool ) ( hPath , exportedMd string ) {
bt := treenode . GetBlockTree ( id )
if nil == bt {
return
}
tree := prepareExportTree ( bt )
hPath = tree . HPath
exportedMd = exportMarkdownContent0 ( tree , "" , false ,
".md" , refMode , embedMode , Conf . Export . FileAnnotationRefMode ,
Conf . Export . TagOpenMarker , Conf . Export . TagCloseMarker ,
Conf . Export . BlockRefTextLeft , Conf . Export . BlockRefTextRight ,
2025-04-20 17:27:14 +08:00
Conf . Export . AddTitle , Conf . Export . InlineMemo , nil , true , & map [ string ] * parse . Tree { } )
2025-02-09 12:20:18 +08:00
docIAL := parse . IAL2Map ( tree . Root . KramdownIAL )
if addYfm {
exportedMd = yfm ( docIAL ) + exportedMd
}
return
2022-05-26 15:18:53 +08:00
}
2024-12-18 17:17:24 +08:00
func exportMarkdownContent ( id , ext string , exportRefMode int , defBlockIDs [ ] string , singleFile bool , treeCache * map [ string ] * parse . Tree ) ( hPath , exportedMd string ) {
tree , err := loadTreeWithCache ( id , treeCache )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-01-02 22:22:11 +08:00
logging . LogErrorf ( "load tree by block id [%s] failed: %s" , id , err )
return
}
2022-05-26 15:18:53 +08:00
hPath = tree . HPath
2023-01-03 00:08:47 +08:00
exportedMd = exportMarkdownContent0 ( tree , "" , false ,
2024-12-18 01:06:58 +08:00
ext , exportRefMode , Conf . Export . BlockEmbedMode , Conf . Export . FileAnnotationRefMode ,
2023-01-02 23:26:01 +08:00
Conf . Export . TagOpenMarker , Conf . Export . TagCloseMarker ,
Conf . Export . BlockRefTextLeft , Conf . Export . BlockRefTextRight ,
2025-04-20 17:27:14 +08:00
Conf . Export . AddTitle , Conf . Export . InlineMemo , defBlockIDs , singleFile , treeCache )
2023-03-23 15:08:55 +08:00
docIAL := parse . IAL2Map ( tree . Root . KramdownIAL )
2025-02-18 15:55:25 +08:00
if Conf . Export . MarkdownYFM {
// 导出 Markdown 时在文档头添加 YFM 开关 https://github.com/siyuan-note/siyuan/issues/7727
exportedMd = yfm ( docIAL ) + exportedMd
}
2023-01-02 21:28:50 +08:00
return
}
2023-01-03 00:08:47 +08:00
func exportMarkdownContent0 ( tree * parse . Tree , cloudAssetsBase string , assetsDestSpace2Underscore bool ,
2024-12-18 01:06:58 +08:00
ext string , blockRefMode , blockEmbedMode , fileAnnotationRefMode int ,
2024-12-17 22:13:37 +08:00
tagOpenMarker , tagCloseMarker string , blockRefTextLeft , blockRefTextRight string ,
2025-04-20 17:27:14 +08:00
addTitle , inlineMemo bool , defBlockIDs [ ] string , singleFile bool , treeCache * map [ string ] * parse . Tree ) ( ret string ) {
2024-08-11 09:33:43 +08:00
tree = exportTree ( tree , false , false , false ,
2023-01-02 23:26:01 +08:00
blockRefMode , blockEmbedMode , fileAnnotationRefMode ,
tagOpenMarker , tagCloseMarker ,
blockRefTextLeft , blockRefTextRight ,
2025-04-20 17:27:14 +08:00
addTitle , inlineMemo , 0 < len ( defBlockIDs ) , singleFile , treeCache )
2022-05-26 15:18:53 +08:00
luteEngine := NewLute ( )
luteEngine . SetFootnotes ( true )
luteEngine . SetKramdownIAL ( false )
2023-01-02 22:22:11 +08:00
if "" != cloudAssetsBase {
2023-01-04 11:26:26 +08:00
luteEngine . RenderOptions . LinkBase = cloudAssetsBase
2023-01-02 22:22:11 +08:00
}
2023-01-03 00:08:47 +08:00
if assetsDestSpace2Underscore { // 上传到社区图床的资源文件会将空格转为下划线,所以这里也需要将文档内容做相应的转换
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if ast . NodeLinkDest == n . Type {
if util . IsAssetLinkDest ( n . Tokens ) {
n . Tokens = bytes . ReplaceAll ( n . Tokens , [ ] byte ( " " ) , [ ] byte ( "_" ) )
}
} else if n . IsTextMarkType ( "a" ) {
href := n . TextMarkAHref
if util . IsAssetLinkDest ( [ ] byte ( href ) ) {
n . TextMarkAHref = strings . ReplaceAll ( href , " " , "_" )
}
2024-09-08 22:58:30 +08:00
} else if ast . NodeIFrame == n . Type || ast . NodeAudio == n . Type || ast . NodeVideo == n . Type {
dest := treenode . GetNodeSrcTokens ( n )
if util . IsAssetLinkDest ( [ ] byte ( dest ) ) {
setAssetsLinkDest ( n , dest , strings . ReplaceAll ( dest , " " , "_" ) )
}
2023-01-03 00:08:47 +08:00
}
return ast . WalkContinue
} )
}
2024-11-26 10:53:44 +08:00
currentDocDir := path . Dir ( tree . HPath )
2023-10-25 21:59:36 +08:00
var unlinks [ ] * ast . Node
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if ast . NodeBr == n . Type {
if ! n . ParentIs ( ast . NodeTableCell ) {
2024-01-28 00:23:47 +08:00
// When exporting Markdown, `<br />` nodes in non-tables are replaced with `\n` text nodes https://github.com/siyuan-note/siyuan/issues/9509
2023-10-25 21:59:36 +08:00
n . InsertBefore ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( "\n" ) } )
unlinks = append ( unlinks , n )
}
}
2024-01-28 00:23:47 +08:00
2024-12-08 17:08:18 +08:00
if 4 == blockRefMode { // 脚注+锚点哈希
2024-01-28 00:23:47 +08:00
if n . IsBlock ( ) && gulu . Str . Contains ( n . ID , defBlockIDs ) {
// 如果是定义块,则在开头处添加锚点
2024-12-09 17:31:42 +08:00
anchorSpan := treenode . NewSpanAnchor ( n . ID )
2024-01-28 00:23:47 +08:00
if ast . NodeDocument != n . Type {
2024-01-28 10:51:31 +08:00
firstLeaf := treenode . FirstLeafBlock ( n )
2024-11-27 20:55:13 +08:00
if nil != firstLeaf {
if ast . NodeTable == firstLeaf . Type {
firstLeaf . InsertBefore ( anchorSpan )
firstLeaf . InsertBefore ( & ast . Node { Type : ast . NodeHardBreak } )
} else {
if nil != firstLeaf . FirstChild {
firstLeaf . FirstChild . InsertBefore ( anchorSpan )
} else {
firstLeaf . AppendChild ( anchorSpan )
}
}
2024-01-28 00:23:47 +08:00
} else {
n . AppendChild ( anchorSpan )
}
}
}
if treenode . IsBlockRef ( n ) {
// 如果是引用元素,则将其转换为超链接,指向 xxx.md#block-id
defID , linkText := getExportBlockRefLinkText ( n , blockRefTextLeft , blockRefTextRight )
if gulu . Str . Contains ( defID , defBlockIDs ) {
var href string
bt := treenode . GetBlockTree ( defID )
if nil != bt {
2024-12-18 01:06:58 +08:00
href += bt . HPath + ext
2024-01-28 00:23:47 +08:00
if "d" != bt . Type {
href += "#" + defID
}
2024-11-26 10:58:25 +08:00
if tree . ID == bt . RootID {
href = "#" + defID
}
2024-01-28 00:23:47 +08:00
}
2024-11-26 10:53:44 +08:00
href = strings . TrimPrefix ( href , currentDocDir )
2024-11-26 10:34:22 +08:00
href = util . FilterFilePath ( href )
2024-11-26 10:53:44 +08:00
href = strings . TrimPrefix ( href , "/" )
2024-01-28 00:23:47 +08:00
blockRefLink := & ast . Node { Type : ast . NodeTextMark , TextMarkType : "a" , TextMarkTextContent : linkText , TextMarkAHref : href }
blockRefLink . KramdownIAL = n . KramdownIAL
n . InsertBefore ( blockRefLink )
unlinks = append ( unlinks , n )
}
}
}
2023-10-25 21:59:36 +08:00
return ast . WalkContinue
} )
for _ , unlink := range unlinks {
unlink . Unlink ( )
}
2025-04-22 20:50:05 +08:00
luteEngine . SetUnorderedListMarker ( "-" )
2022-09-15 11:46:41 +08:00
renderer := render . NewProtyleExportMdRenderer ( tree , luteEngine . RenderOptions )
2023-01-02 21:28:50 +08:00
ret = gulu . Str . FromBytes ( renderer . Render ( ) )
2022-05-26 15:18:53 +08:00
return
}
2024-08-11 09:33:43 +08:00
func exportTree ( tree * parse . Tree , wysiwyg , keepFold , avHiddenCol bool ,
2023-01-02 23:26:01 +08:00
blockRefMode , blockEmbedMode , fileAnnotationRefMode int ,
tagOpenMarker , tagCloseMarker string ,
blockRefTextLeft , blockRefTextRight string ,
2025-04-20 17:27:14 +08:00
addTitle , inlineMemo , addDocAnchorSpan , singleFile bool , treeCache * map [ string ] * parse . Tree ) ( ret * parse . Tree ) {
2022-05-26 15:18:53 +08:00
luteEngine := NewLute ( )
ret = tree
id := tree . Root . ID
2025-01-16 17:23:02 +08:00
( * treeCache ) [ tree . ID ] = tree
2022-05-26 15:18:53 +08:00
// 解析查询嵌入节点
2024-08-15 18:07:40 +08:00
depth := 0
resolveEmbedR ( ret . Root , blockEmbedMode , luteEngine , & [ ] string { } , & depth )
2022-05-26 15:18:53 +08:00
2024-12-01 12:25:00 +08:00
// 将块超链接转换为引用
depth = 0
2024-12-18 17:17:24 +08:00
blockLink2Ref ( ret , ret . ID , treeCache , & depth )
2024-12-01 12:25:00 +08:00
2024-12-08 17:08:18 +08:00
// 收集引用转脚注+锚点哈希
2022-05-26 15:18:53 +08:00
var refFootnotes [ ] * refAsFootnotes
2024-12-17 22:48:16 +08:00
if 4 == blockRefMode && singleFile {
2024-12-01 12:25:00 +08:00
depth = 0
2024-12-18 17:17:24 +08:00
collectFootnotesDefs ( ret , ret . ID , & refFootnotes , treeCache , & depth )
2022-05-26 15:18:53 +08:00
}
2024-11-28 09:42:31 +08:00
currentTreeNodeIDs := map [ string ] bool { }
ast . Walk ( ret . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if "" != n . ID {
currentTreeNodeIDs [ n . ID ] = true
}
return ast . WalkContinue
} )
2024-06-16 00:08:15 +08:00
var unlinks [ ] * ast . Node
2022-05-26 15:18:53 +08:00
ast . Walk ( ret . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
switch n . Type {
case ast . NodeSuperBlockOpenMarker , ast . NodeSuperBlockLayoutMarker , ast . NodeSuperBlockCloseMarker :
if ! wysiwyg {
unlinks = append ( unlinks , n )
return ast . WalkContinue
}
case ast . NodeHeading :
2024-11-28 09:42:31 +08:00
n . SetIALAttr ( "id" , n . ID )
2022-12-08 20:19:35 +08:00
case ast . NodeMathBlockContent :
2022-05-26 15:18:53 +08:00
n . Tokens = bytes . TrimSpace ( n . Tokens ) // 导出 Markdown 时去除公式内容中的首尾空格 https://github.com/siyuan-note/siyuan/issues/4666
return ast . WalkContinue
2022-09-16 20:50:14 +08:00
case ast . NodeTextMark :
2025-04-21 00:44:51 +08:00
if n . IsTextMarkType ( "inline-memo" ) {
if ! inlineMemo {
n . TextMarkInlineMemoContent = ""
}
}
2022-09-16 20:50:14 +08:00
if n . IsTextMarkType ( "inline-math" ) {
n . TextMarkInlineMathContent = strings . TrimSpace ( n . TextMarkInlineMathContent )
return ast . WalkContinue
2023-02-10 15:21:20 +08:00
} else if treenode . IsFileAnnotationRef ( n ) {
2022-09-16 22:59:24 +08:00
refID := n . TextMarkFileAnnotationRefID
2023-04-28 19:31:51 +08:00
if ! strings . Contains ( refID , "/" ) {
return ast . WalkSkipChildren
}
2024-06-24 23:11:41 +08:00
status := processFileAnnotationRef ( refID , n , fileAnnotationRefMode )
2022-09-16 22:59:24 +08:00
unlinks = append ( unlinks , n )
return status
2022-09-20 11:26:53 +08:00
} else if n . IsTextMarkType ( "tag" ) {
if ! wysiwyg {
n . Type = ast . NodeText
2023-01-02 23:26:01 +08:00
n . Tokens = [ ] byte ( tagOpenMarker + n . TextMarkTextContent + tagCloseMarker )
2022-09-20 11:26:53 +08:00
return ast . WalkContinue
}
2022-09-16 20:50:14 +08:00
}
2022-05-26 15:18:53 +08:00
}
2022-09-16 18:02:04 +08:00
if ! treenode . IsBlockRef ( n ) {
2022-05-26 15:18:53 +08:00
return ast . WalkContinue
}
// 处理引用节点
2024-01-28 00:23:47 +08:00
defID , linkText := getExportBlockRefLinkText ( n , blockRefTextLeft , blockRefTextRight )
2022-05-26 15:18:53 +08:00
2023-01-02 23:26:01 +08:00
switch blockRefMode {
2022-05-26 15:18:53 +08:00
case 2 : // 锚文本块链
2024-01-28 00:23:47 +08:00
blockRefLink := & ast . Node { Type : ast . NodeTextMark , TextMarkType : "a" , TextMarkTextContent : linkText , TextMarkAHref : "siyuan://blocks/" + defID }
2024-01-15 22:52:22 +08:00
blockRefLink . KramdownIAL = n . KramdownIAL
2025-04-21 00:44:51 +08:00
if n . IsTextMarkType ( "inline-memo" ) {
blockRefLink . TextMarkInlineMemoContent = n . TextMarkInlineMemoContent
blockRefLink . TextMarkType = "a inline-memo"
}
2022-05-26 15:18:53 +08:00
n . InsertBefore ( blockRefLink )
2024-01-28 00:23:47 +08:00
unlinks = append ( unlinks , n )
2022-05-26 15:18:53 +08:00
case 3 : // 仅锚文本
2024-07-16 10:44:57 +08:00
var blockRefLink * ast . Node
if 0 < len ( n . KramdownIAL ) {
blockRefLink = & ast . Node { Type : ast . NodeTextMark , TextMarkType : "text" , TextMarkTextContent : linkText }
blockRefLink . KramdownIAL = n . KramdownIAL
2025-04-21 00:44:51 +08:00
if n . IsTextMarkType ( "inline-memo" ) {
blockRefLink . TextMarkInlineMemoContent = n . TextMarkInlineMemoContent
blockRefLink . TextMarkType = "text inline-memo"
}
2024-07-16 10:44:57 +08:00
} else {
blockRefLink = & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( linkText ) }
2025-04-21 00:44:51 +08:00
if n . IsTextMarkType ( "inline-memo" ) {
blockRefLink . Type = ast . NodeTextMark
blockRefLink . TextMarkInlineMemoContent = n . TextMarkInlineMemoContent
blockRefLink . TextMarkType = "inline-memo"
blockRefLink . TextMarkTextContent = linkText
}
2024-07-16 10:44:57 +08:00
}
2024-01-15 22:52:22 +08:00
n . InsertBefore ( blockRefLink )
2024-01-28 00:23:47 +08:00
unlinks = append ( unlinks , n )
2024-12-08 17:08:18 +08:00
case 4 : // 脚注+锚点哈希
2024-11-28 09:42:31 +08:00
if currentTreeNodeIDs [ defID ] {
// 当前文档内不转换脚注,直接使用锚点哈希 https://github.com/siyuan-note/siyuan/issues/13283
2024-12-08 18:38:38 +08:00
n . TextMarkType = strings . ReplaceAll ( n . TextMarkType , "block-ref" , "a" )
2024-11-28 09:42:31 +08:00
n . TextMarkTextContent = linkText
n . TextMarkAHref = "#" + defID
return ast . WalkContinue
}
2022-05-26 15:18:53 +08:00
refFoot := getRefAsFootnotes ( defID , & refFootnotes )
2024-11-28 09:42:31 +08:00
if nil == refFoot {
return ast . WalkContinue
}
2025-04-21 00:44:51 +08:00
text := & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( linkText ) }
n . InsertBefore ( text )
if n . IsTextMarkType ( "inline-memo" ) {
text . Type = ast . NodeTextMark
text . TextMarkType = "inline-memo"
2025-04-23 10:51:53 +08:00
text . TextMarkTextContent = linkText
2025-04-21 00:44:51 +08:00
text . TextMarkInlineMemoContent = n . TextMarkInlineMemoContent
}
2022-05-26 15:18:53 +08:00
n . InsertBefore ( & ast . Node { Type : ast . NodeFootnotesRef , Tokens : [ ] byte ( "^" + refFoot . refNum ) , FootnotesRefId : refFoot . refNum , FootnotesRefLabel : [ ] byte ( "^" + refFoot . refNum ) } )
2024-01-28 00:23:47 +08:00
unlinks = append ( unlinks , n )
2022-05-26 15:18:53 +08:00
}
2024-01-28 00:23:47 +08:00
2022-09-17 16:58:16 +08:00
if nil != n . Next && ast . NodeKramdownSpanIAL == n . Next . Type {
// 引用加排版标记(比如颜色)重叠时丢弃后面的排版属性节点
unlinks = append ( unlinks , n . Next )
}
2022-05-26 15:18:53 +08:00
return ast . WalkSkipChildren
} )
for _ , n := range unlinks {
n . Unlink ( )
}
2024-12-08 17:08:18 +08:00
if 4 == blockRefMode { // 脚注+锚点哈希
2024-03-19 10:58:16 +08:00
unlinks = nil
2024-12-18 17:17:24 +08:00
footnotesDefBlock := resolveFootnotesDefs ( & refFootnotes , ret , currentTreeNodeIDs , blockRefTextLeft , blockRefTextRight , treeCache )
2024-11-28 13:25:24 +08:00
if nil != footnotesDefBlock {
2024-03-19 10:58:16 +08:00
// 如果是聚焦导出,可能存在没有使用的脚注定义块,在这里进行清理
2024-03-19 10:59:08 +08:00
// Improve focus export conversion of block refs to footnotes https://github.com/siyuan-note/siyuan/issues/10647
2024-11-28 13:25:24 +08:00
footnotesRefs := ret . Root . ChildrenByType ( ast . NodeFootnotesRef )
2024-11-28 15:37:45 +08:00
for footnotesDef := footnotesDefBlock . FirstChild ; nil != footnotesDef ; footnotesDef = footnotesDef . Next {
fnRefsInDef := footnotesDef . ChildrenByType ( ast . NodeFootnotesRef )
footnotesRefs = append ( footnotesRefs , fnRefsInDef ... )
}
2024-11-28 13:25:24 +08:00
for footnotesDef := footnotesDefBlock . FirstChild ; nil != footnotesDef ; footnotesDef = footnotesDef . Next {
exist := false
for _ , ref := range footnotesRefs {
if ref . FootnotesRefId == footnotesDef . FootnotesRefId {
exist = true
break
}
}
if ! exist {
unlinks = append ( unlinks , footnotesDef )
}
}
for _ , n := range unlinks {
n . Unlink ( )
}
2024-03-19 10:58:16 +08:00
2022-05-26 15:18:53 +08:00
ret . Root . AppendChild ( footnotesDefBlock )
}
}
2023-01-02 23:26:01 +08:00
if addTitle {
2023-01-27 17:56:29 +08:00
if root , _ := getBlock ( id , tree ) ; nil != root {
2024-03-16 12:22:18 +08:00
root . IAL [ "type" ] = "doc"
2024-09-18 12:24:23 +08:00
title := & ast . Node { Type : ast . NodeHeading , HeadingLevel : 1 }
2025-05-26 22:15:41 +08:00
for k , v := range root . IAL {
if "type" == k {
continue
}
title . SetIALAttr ( k , v )
}
title . InsertAfter ( & ast . Node { Type : ast . NodeKramdownBlockIAL , Tokens : parse . IAL2Tokens ( title . KramdownIAL ) } )
2022-09-08 20:49:55 +08:00
content := html . UnescapeString ( root . Content )
title . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( content ) } )
2022-05-26 15:18:53 +08:00
ret . Root . PrependChild ( title )
}
2024-12-08 17:35:41 +08:00
} else {
if 4 == blockRefMode { // 脚注+锚点哈希
2025-04-27 16:37:06 +08:00
refRoot := false
for _ , refFoot := range refFootnotes {
if id == refFoot . defID {
refRoot = true
break
}
}
footnotesDefs := tree . Root . ChildrenByType ( ast . NodeFootnotesDef )
for _ , footnotesDef := range footnotesDefs {
ast . Walk ( footnotesDef , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if id == n . TextMarkBlockRefID {
refRoot = true
return ast . WalkStop
}
return ast . WalkContinue
} )
}
if refRoot && addDocAnchorSpan {
2024-12-17 22:13:37 +08:00
anchorSpan := treenode . NewSpanAnchor ( id )
ret . Root . PrependChild ( anchorSpan )
}
2024-12-08 17:35:41 +08:00
}
2022-05-26 15:18:53 +08:00
}
// 导出时支持导出题头图 https://github.com/siyuan-note/siyuan/issues/4372
titleImgPath := treenode . GetDocTitleImgPath ( ret . Root )
if "" != titleImgPath {
p := & ast . Node { Type : ast . NodeParagraph }
titleImg := & ast . Node { Type : ast . NodeImage }
titleImg . AppendChild ( & ast . Node { Type : ast . NodeBang } )
titleImg . AppendChild ( & ast . Node { Type : ast . NodeOpenBracket } )
titleImg . AppendChild ( & ast . Node { Type : ast . NodeLinkText , Tokens : [ ] byte ( "image" ) } )
titleImg . AppendChild ( & ast . Node { Type : ast . NodeCloseBracket } )
titleImg . AppendChild ( & ast . Node { Type : ast . NodeOpenParen } )
titleImg . AppendChild ( & ast . Node { Type : ast . NodeLinkDest , Tokens : [ ] byte ( titleImgPath ) } )
titleImg . AppendChild ( & ast . Node { Type : ast . NodeCloseParen } )
p . AppendChild ( titleImg )
ret . Root . PrependChild ( p )
}
unlinks = nil
2022-09-03 22:56:13 +08:00
var emptyParagraphs [ ] * ast . Node
2022-05-26 15:18:53 +08:00
ast . Walk ( ret . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
2022-09-24 21:50:22 +08:00
// 支持按照现有折叠状态导出 PDF https://github.com/siyuan-note/siyuan/issues/5941
if ! keepFold {
// 块折叠以后导出 HTML/PDF 固定展开 https://github.com/siyuan-note/siyuan/issues/4064
n . RemoveIALAttr ( "fold" )
n . RemoveIALAttr ( "heading-fold" )
2022-09-24 23:17:23 +08:00
} else {
if "1" == n . IALAttr ( "heading-fold" ) {
unlinks = append ( unlinks , n )
return ast . WalkContinue
}
2022-09-24 21:50:22 +08:00
}
2022-05-26 15:18:53 +08:00
2023-02-15 17:59:57 +08:00
// 导出时去掉内容块闪卡样式 https://github.com/siyuan-note/siyuan/issues/7374
if n . IsBlock ( ) {
n . RemoveIALAttr ( "custom-riff-decks" )
}
2022-12-04 12:13:51 +08:00
switch n . Type {
case ast . NodeParagraph :
2022-09-03 22:56:13 +08:00
if nil == n . FirstChild {
// 空的段落块需要补全文本展位,否则后续格式化后再解析树会语义不一致 https://github.com/siyuan-note/siyuan/issues/5806
emptyParagraphs = append ( emptyParagraphs , n )
}
2022-12-04 12:13:51 +08:00
case ast . NodeWidget :
2022-10-13 22:25:31 +08:00
// 挂件块导出 https://github.com/siyuan-note/siyuan/issues/3834 https://github.com/siyuan-note/siyuan/issues/6188
if wysiwyg {
exportHtmlVal := n . IALAttr ( "data-export-html" )
if "" != exportHtmlVal {
htmlBlock := & ast . Node { Type : ast . NodeHTMLBlock , Tokens : [ ] byte ( exportHtmlVal ) }
n . InsertBefore ( htmlBlock )
unlinks = append ( unlinks , n )
return ast . WalkContinue
}
}
2022-05-26 15:18:53 +08:00
exportMdVal := n . IALAttr ( "data-export-md" )
exportMdVal = html . UnescapeString ( exportMdVal ) // 导出 `data-export-md` 时未解析代码块与行内代码内的转义字符 https://github.com/siyuan-note/siyuan/issues/4180
if "" != exportMdVal {
2023-03-23 15:23:10 +08:00
luteEngine0 := util . NewLute ( )
luteEngine0 . SetYamlFrontMatter ( true ) // 挂件导出属性 `data-export-md` 支持 YFM https://github.com/siyuan-note/siyuan/issues/7752
2023-03-29 19:50:29 +08:00
exportMdTree := parse . Parse ( "" , [ ] byte ( exportMdVal ) , luteEngine0 . ParseOptions )
2022-05-26 15:18:53 +08:00
var insertNodes [ ] * ast . Node
for c := exportMdTree . Root . FirstChild ; nil != c ; c = c . Next {
if ast . NodeKramdownBlockIAL != c . Type {
insertNodes = append ( insertNodes , c )
}
}
for _ , insertNode := range insertNodes {
n . InsertBefore ( insertNode )
}
unlinks = append ( unlinks , n )
}
2022-12-04 12:13:51 +08:00
case ast . NodeSuperBlockOpenMarker , ast . NodeSuperBlockLayoutMarker , ast . NodeSuperBlockCloseMarker :
if ! wysiwyg {
unlinks = append ( unlinks , n )
}
2022-05-26 15:18:53 +08:00
}
if ast . NodeText != n . Type {
return ast . WalkContinue
}
2022-12-04 12:13:51 +08:00
2022-05-26 15:18:53 +08:00
// Shift+Enter 换行在导出为 Markdown 时使用硬换行 https://github.com/siyuan-note/siyuan/issues/3458
n . Tokens = bytes . ReplaceAll ( n . Tokens , [ ] byte ( "\n" ) , [ ] byte ( " \n" ) )
return ast . WalkContinue
} )
for _ , n := range unlinks {
n . Unlink ( )
}
2022-09-03 22:56:13 +08:00
for _ , emptyParagraph := range emptyParagraphs {
2022-09-14 00:35:13 +08:00
emptyParagraph . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( editor . Zwj ) } )
2022-09-03 22:56:13 +08:00
}
2023-09-06 11:42:35 +08:00
unlinks = nil
// Attribute View export https://github.com/siyuan-note/siyuan/issues/8710
ast . Walk ( ret . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if ast . NodeAttributeView != n . Type {
return ast . WalkContinue
}
avID := n . AttributeViewID
2023-11-06 22:13:04 +08:00
if avJSONPath := av . GetAttributeViewDataPath ( avID ) ; ! filelock . IsExist ( avJSONPath ) {
2023-09-06 11:42:35 +08:00
return ast . WalkContinue
}
attrView , err := av . ParseAttributeView ( avID )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-09-06 11:42:35 +08:00
logging . LogErrorf ( "parse attribute view [%s] failed: %s" , avID , err )
return ast . WalkContinue
}
2024-03-04 15:57:35 +08:00
viewID := n . IALAttr ( av . NodeAttrView )
view , err := attrView . GetCurrentView ( viewID )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-10-24 10:15:01 +08:00
logging . LogErrorf ( "get attribute view [%s] failed: %s" , avID , err )
return ast . WalkContinue
2023-09-06 11:42:35 +08:00
}
2024-10-17 23:31:54 +08:00
table := sql . RenderAttributeViewTable ( attrView , view , "" )
2023-09-06 11:42:35 +08:00
2024-02-29 22:49:02 +08:00
// 遵循视图过滤和排序规则 Use filtering and sorting of current view settings when exporting database blocks https://github.com/siyuan-note/siyuan/issues/10474
table . FilterRows ( attrView )
2024-03-21 10:03:08 +08:00
table . SortRows ( attrView )
2024-02-29 22:49:02 +08:00
2023-09-06 11:42:35 +08:00
var aligns [ ] int
for range table . Columns {
aligns = append ( aligns , 0 )
}
mdTable := & ast . Node { Type : ast . NodeTable , TableAligns : aligns }
mdTableHead := & ast . Node { Type : ast . NodeTableHead }
mdTable . AppendChild ( mdTableHead )
mdTableHeadRow := & ast . Node { Type : ast . NodeTableRow , TableAligns : aligns }
mdTableHead . AppendChild ( mdTableHeadRow )
for _ , col := range table . Columns {
2024-08-11 09:33:43 +08:00
if avHiddenCol && col . Hidden {
// 按需跳过隐藏列 Improve database table view exporting https://github.com/siyuan-note/siyuan/issues/12232
continue
}
2023-09-06 11:42:35 +08:00
cell := & ast . Node { Type : ast . NodeTableCell }
2024-08-17 08:32:49 +08:00
name := col . Name
if ! wysiwyg {
name = string ( lex . EscapeProtyleMarkers ( [ ] byte ( col . Name ) ) )
name = strings . ReplaceAll ( name , "\\|" , "|" )
name = strings . ReplaceAll ( name , "|" , "\\|" )
}
2024-07-11 09:48:36 +08:00
cell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( name ) } )
2023-09-06 11:42:35 +08:00
mdTableHeadRow . AppendChild ( cell )
}
2024-04-30 17:46:32 +08:00
rowNum := 1
2023-09-06 11:42:35 +08:00
for _ , row := range table . Rows {
mdTableRow := & ast . Node { Type : ast . NodeTableRow , TableAligns : aligns }
mdTable . AppendChild ( mdTableRow )
for _ , cell := range row . Cells {
2024-08-11 09:33:43 +08:00
if avHiddenCol && nil != cell . Value {
if col := table . GetColumn ( cell . Value . KeyID ) ; nil != col && col . Hidden {
continue
}
}
2023-09-06 11:42:35 +08:00
mdTableCell := & ast . Node { Type : ast . NodeTableCell }
mdTableRow . AppendChild ( mdTableCell )
var val string
if nil != cell . Value {
2024-07-14 10:14:32 +08:00
if av . KeyTypeBlock == cell . Value . Type {
if nil != cell . Value . Block {
val = cell . Value . Block . Content
2024-08-17 08:32:49 +08:00
if ! wysiwyg {
val = string ( lex . EscapeProtyleMarkers ( [ ] byte ( val ) ) )
val = strings . ReplaceAll ( val , "\\|" , "|" )
val = strings . ReplaceAll ( val , "|" , "\\|" )
}
2024-07-16 23:44:15 +08:00
col := table . GetColumn ( cell . Value . KeyID )
if nil != col && col . Wrap {
lines := strings . Split ( val , "\n" )
for _ , line := range lines {
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( line ) } )
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeHardBreak } )
}
} else {
val = strings . ReplaceAll ( val , "\n" , " " )
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( val ) } )
2024-07-14 10:14:32 +08:00
}
continue
}
} else if av . KeyTypeText == cell . Value . Type {
2024-07-11 09:48:36 +08:00
if nil != cell . Value . Text {
val = cell . Value . Text . Content
2024-08-17 08:32:49 +08:00
if ! wysiwyg {
val = string ( lex . EscapeProtyleMarkers ( [ ] byte ( val ) ) )
val = strings . ReplaceAll ( val , "\\|" , "|" )
val = strings . ReplaceAll ( val , "|" , "\\|" )
}
2024-07-16 23:44:15 +08:00
col := table . GetColumn ( cell . Value . KeyID )
if nil != col && col . Wrap {
lines := strings . Split ( val , "\n" )
for _ , line := range lines {
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( line ) } )
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeHardBreak } )
}
} else {
val = strings . ReplaceAll ( val , "\n" , " " )
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( val ) } )
}
continue
}
} else if av . KeyTypeTemplate == cell . Value . Type {
if nil != cell . Value . Template {
val = cell . Value . Template . Content
if "<no value>" == val {
val = ""
}
val = strings . ReplaceAll ( val , "\\|" , "|" )
val = strings . ReplaceAll ( val , "|" , "\\|" )
col := table . GetColumn ( cell . Value . KeyID )
if nil != col && col . Wrap {
lines := strings . Split ( val , "\n" )
for _ , line := range lines {
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( line ) } )
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeHardBreak } )
}
} else {
val = strings . ReplaceAll ( val , "\n" , " " )
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( val ) } )
2024-07-11 09:48:36 +08:00
}
continue
}
} else if av . KeyTypeDate == cell . Value . Type {
2023-10-08 12:16:58 +08:00
if nil != cell . Value . Date {
2024-02-23 17:53:43 +08:00
cell . Value . Date = av . NewFormattedValueDate ( cell . Value . Date . Content , cell . Value . Date . Content2 , av . DateFormatNone , cell . Value . Date . IsNotTime , cell . Value . Date . HasEndDate )
2023-10-08 12:16:58 +08:00
}
} else if av . KeyTypeCreated == cell . Value . Type {
if nil != cell . Value . Created {
2023-10-10 20:37:51 +08:00
cell . Value . Created = av . NewFormattedValueCreated ( cell . Value . Created . Content , 0 , av . CreatedFormatNone )
2023-10-08 12:16:58 +08:00
}
} else if av . KeyTypeUpdated == cell . Value . Type {
if nil != cell . Value . Updated {
2023-10-10 20:37:51 +08:00
cell . Value . Updated = av . NewFormattedValueUpdated ( cell . Value . Updated . Content , 0 , av . UpdatedFormatNone )
2023-10-08 12:16:58 +08:00
}
2024-09-15 11:46:35 +08:00
} else if av . KeyTypeURL == cell . Value . Type {
if nil != cell . Value . URL {
if "" != strings . TrimSpace ( cell . Value . URL . Content ) {
link := & ast . Node { Type : ast . NodeLink }
link . AppendChild ( & ast . Node { Type : ast . NodeOpenBracket } )
link . AppendChild ( & ast . Node { Type : ast . NodeLinkText , Tokens : [ ] byte ( cell . Value . URL . Content ) } )
link . AppendChild ( & ast . Node { Type : ast . NodeCloseBracket } )
link . AppendChild ( & ast . Node { Type : ast . NodeOpenParen } )
link . AppendChild ( & ast . Node { Type : ast . NodeLinkDest , Tokens : [ ] byte ( cell . Value . URL . Content ) } )
link . AppendChild ( & ast . Node { Type : ast . NodeCloseParen } )
mdTableCell . AppendChild ( link )
}
continue
}
2023-11-30 12:03:16 +08:00
} else if av . KeyTypeMAsset == cell . Value . Type {
if nil != cell . Value . MAsset {
2024-09-15 11:32:04 +08:00
for i , a := range cell . Value . MAsset {
2024-04-30 17:46:32 +08:00
if av . AssetTypeImage == a . Type {
2024-07-10 22:32:42 +08:00
img := & ast . Node { Type : ast . NodeImage }
img . AppendChild ( & ast . Node { Type : ast . NodeBang } )
img . AppendChild ( & ast . Node { Type : ast . NodeOpenBracket } )
img . AppendChild ( & ast . Node { Type : ast . NodeLinkText , Tokens : [ ] byte ( a . Name ) } )
img . AppendChild ( & ast . Node { Type : ast . NodeCloseBracket } )
img . AppendChild ( & ast . Node { Type : ast . NodeOpenParen } )
img . AppendChild ( & ast . Node { Type : ast . NodeLinkDest , Tokens : [ ] byte ( a . Content ) } )
img . AppendChild ( & ast . Node { Type : ast . NodeCloseParen } )
mdTableCell . AppendChild ( img )
2024-04-30 17:46:32 +08:00
} else if av . AssetTypeFile == a . Type {
2024-09-15 11:32:04 +08:00
linkText := strings . TrimSpace ( a . Name )
if "" == linkText {
linkText = a . Content
}
2024-09-15 11:46:35 +08:00
if "" != strings . TrimSpace ( a . Content ) {
file := & ast . Node { Type : ast . NodeLink }
file . AppendChild ( & ast . Node { Type : ast . NodeOpenBracket } )
file . AppendChild ( & ast . Node { Type : ast . NodeLinkText , Tokens : [ ] byte ( linkText ) } )
file . AppendChild ( & ast . Node { Type : ast . NodeCloseBracket } )
file . AppendChild ( & ast . Node { Type : ast . NodeOpenParen } )
file . AppendChild ( & ast . Node { Type : ast . NodeLinkDest , Tokens : [ ] byte ( a . Content ) } )
file . AppendChild ( & ast . Node { Type : ast . NodeCloseParen } )
mdTableCell . AppendChild ( file )
} else {
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( linkText ) } )
}
2024-04-30 17:46:32 +08:00
}
2024-09-15 11:32:04 +08:00
if i < len ( cell . Value . MAsset ) - 1 {
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( " " ) } )
}
2023-11-30 12:03:16 +08:00
}
2024-07-10 22:32:42 +08:00
continue
2023-11-30 12:03:16 +08:00
}
2024-04-30 17:46:32 +08:00
} else if av . KeyTypeLineNumber == cell . Value . Type {
val = strconv . Itoa ( rowNum )
rowNum ++
2024-09-10 18:17:20 +08:00
} else if av . KeyTypeRelation == cell . Value . Type {
for i , v := range cell . Value . Relation . Contents {
if nil == v {
continue
}
if av . KeyTypeBlock == v . Type && nil != v . Block {
val = v . Block . Content
if ! wysiwyg {
val = string ( lex . EscapeProtyleMarkers ( [ ] byte ( val ) ) )
val = strings . ReplaceAll ( val , "\\|" , "|" )
val = strings . ReplaceAll ( val , "|" , "\\|" )
}
2024-09-13 21:59:54 +08:00
col := table . GetColumn ( cell . Value . KeyID )
if nil != col && col . Wrap {
lines := strings . Split ( val , "\n" )
for _ , line := range lines {
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( line ) } )
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeHardBreak } )
}
} else {
val = strings . ReplaceAll ( val , "\n" , " " )
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( val ) } )
}
2024-09-10 18:17:20 +08:00
}
if i < len ( cell . Value . Relation . Contents ) - 1 {
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( ", " ) } )
}
}
continue
} else if av . KeyTypeRollup == cell . Value . Type {
for i , v := range cell . Value . Rollup . Contents {
if nil == v {
continue
}
if av . KeyTypeBlock == v . Type {
if nil != v . Block {
val = v . Block . Content
if ! wysiwyg {
val = string ( lex . EscapeProtyleMarkers ( [ ] byte ( val ) ) )
val = strings . ReplaceAll ( val , "\\|" , "|" )
val = strings . ReplaceAll ( val , "|" , "\\|" )
}
2024-09-13 21:59:54 +08:00
col := table . GetColumn ( cell . Value . KeyID )
if nil != col && col . Wrap {
lines := strings . Split ( val , "\n" )
for _ , line := range lines {
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( line ) } )
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeHardBreak } )
}
} else {
val = strings . ReplaceAll ( val , "\n" , " " )
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( val ) } )
}
2024-09-10 18:17:20 +08:00
}
} else if av . KeyTypeText == v . Type {
val = v . Text . Content
if ! wysiwyg {
val = string ( lex . EscapeProtyleMarkers ( [ ] byte ( val ) ) )
val = strings . ReplaceAll ( val , "\\|" , "|" )
val = strings . ReplaceAll ( val , "|" , "\\|" )
}
2024-09-13 21:59:54 +08:00
col := table . GetColumn ( cell . Value . KeyID )
if nil != col && col . Wrap {
lines := strings . Split ( val , "\n" )
for _ , line := range lines {
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( line ) } )
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeHardBreak } )
}
} else {
val = strings . ReplaceAll ( val , "\n" , " " )
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( val ) } )
}
2024-09-10 18:17:20 +08:00
} else {
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( v . String ( true ) ) } )
}
if i < len ( cell . Value . Rollup . Contents ) - 1 {
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( ", " ) } )
}
}
continue
2023-09-06 16:14:43 +08:00
}
2024-04-30 17:46:32 +08:00
if "" == val {
val = cell . Value . String ( true )
}
2023-09-06 11:42:35 +08:00
}
mdTableCell . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( val ) } )
}
}
n . InsertBefore ( mdTable )
unlinks = append ( unlinks , n )
return ast . WalkContinue
} )
for _ , n := range unlinks {
n . Unlink ( )
}
2022-05-26 15:18:53 +08:00
return ret
}
2024-12-01 20:49:18 +08:00
func resolveFootnotesDefs ( refFootnotes * [ ] * refAsFootnotes , currentTree * parse . Tree , currentTreeNodeIDs map [ string ] bool , blockRefTextLeft , blockRefTextRight string , treeCache * map [ string ] * parse . Tree ) ( footnotesDefBlock * ast . Node ) {
2022-05-26 15:18:53 +08:00
if 1 > len ( * refFootnotes ) {
return nil
}
footnotesDefBlock = & ast . Node { Type : ast . NodeFootnotesDefBlock }
2022-08-02 11:17:08 +08:00
var rendered [ ] string
2022-05-26 15:18:53 +08:00
for _ , foot := range * refFootnotes {
2024-12-18 01:14:47 +08:00
t , err := loadTreeWithCache ( foot . defID , treeCache )
if nil != err {
return
2022-05-26 15:18:53 +08:00
}
2024-12-01 20:49:18 +08:00
2022-05-26 15:18:53 +08:00
defNode := treenode . GetNodeInTree ( t , foot . defID )
2024-11-29 08:41:43 +08:00
docID := util . GetTreeID ( defNode . Path )
2022-05-26 15:18:53 +08:00
var nodes [ ] * ast . Node
if ast . NodeHeading == defNode . Type {
nodes = append ( nodes , defNode )
2024-11-28 13:06:25 +08:00
if currentTree . ID != docID {
2022-09-23 19:42:27 +08:00
// 同文档块引转脚注缩略定义考虑容器块和标题块 https://github.com/siyuan-note/siyuan/issues/5917
children := treenode . HeadingChildren ( defNode )
nodes = append ( nodes , children ... )
}
2022-05-26 15:18:53 +08:00
} else if ast . NodeDocument == defNode . Type {
docTitle := & ast . Node { ID : defNode . ID , Type : ast . NodeHeading , HeadingLevel : 1 }
docTitle . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( defNode . IALAttr ( "title" ) ) } )
nodes = append ( nodes , docTitle )
for c := defNode . FirstChild ; nil != c ; c = c . Next {
nodes = append ( nodes , c )
}
} else {
nodes = append ( nodes , defNode )
}
var newNodes [ ] * ast . Node
for _ , node := range nodes {
var unlinks [ ] * ast . Node
ast . Walk ( node , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
2022-09-16 18:02:04 +08:00
if treenode . IsBlockRef ( n ) {
defID , _ , _ := treenode . GetBlockRef ( n )
2022-05-26 15:18:53 +08:00
if f := getRefAsFootnotes ( defID , refFootnotes ) ; nil != f {
2023-01-02 23:26:01 +08:00
n . InsertBefore ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( blockRefTextLeft + f . refAnchorText + blockRefTextRight ) } )
2022-05-26 15:18:53 +08:00
n . InsertBefore ( & ast . Node { Type : ast . NodeFootnotesRef , Tokens : [ ] byte ( "^" + f . refNum ) , FootnotesRefId : f . refNum , FootnotesRefLabel : [ ] byte ( "^" + f . refNum ) } )
unlinks = append ( unlinks , n )
2024-11-28 13:06:25 +08:00
} else {
if isNodeInTree ( defID , currentTree ) {
if currentTreeNodeIDs [ defID ] {
// 当前文档内不转换脚注,直接使用锚点哈希 https://github.com/siyuan-note/siyuan/issues/13283
n . TextMarkType = "a"
n . TextMarkTextContent = blockRefTextLeft + n . TextMarkTextContent + blockRefTextRight
n . TextMarkAHref = "#" + defID
return ast . WalkSkipChildren
}
}
2022-05-26 15:18:53 +08:00
}
return ast . WalkSkipChildren
} else if ast . NodeBlockQueryEmbed == n . Type {
stmt := n . ChildByType ( ast . NodeBlockQueryEmbedScript ) . TokensStr ( )
stmt = html . UnescapeString ( stmt )
2023-03-24 16:37:06 +08:00
stmt = strings . ReplaceAll ( stmt , editor . IALValEscNewLine , "\n" )
2023-04-21 09:49:13 +08:00
sqlBlocks := sql . SelectBlocksRawStmt ( stmt , 1 , Conf . Search . Limit )
2022-05-26 15:18:53 +08:00
for _ , b := range sqlBlocks {
2024-06-16 00:08:15 +08:00
subNodes := renderBlockMarkdownR ( b . ID , & rendered )
2022-05-26 15:18:53 +08:00
for _ , subNode := range subNodes {
if ast . NodeListItem == subNode . Type {
parentList := & ast . Node { Type : ast . NodeList , ListData : & ast . ListData { Typ : subNode . ListData . Typ } }
parentList . AppendChild ( subNode )
newNodes = append ( newNodes , parentList )
} else {
newNodes = append ( newNodes , subNode )
}
}
}
unlinks = append ( unlinks , n )
return ast . WalkSkipChildren
}
return ast . WalkContinue
} )
for _ , n := range unlinks {
n . Unlink ( )
}
if ast . NodeBlockQueryEmbed != node . Type {
if ast . NodeListItem == node . Type {
parentList := & ast . Node { Type : ast . NodeList , ListData : & ast . ListData { Typ : node . ListData . Typ } }
parentList . AppendChild ( node )
newNodes = append ( newNodes , parentList )
} else {
newNodes = append ( newNodes , node )
}
}
}
footnotesDef := & ast . Node { Type : ast . NodeFootnotesDef , Tokens : [ ] byte ( "^" + foot . refNum ) , FootnotesRefId : foot . refNum , FootnotesRefLabel : [ ] byte ( "^" + foot . refNum ) }
for _ , node := range newNodes {
ast . Walk ( node , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if ast . NodeParagraph != n . Type {
return ast . WalkContinue
}
2024-11-29 08:41:43 +08:00
docID := util . GetTreeID ( n . Path )
2024-11-28 13:06:25 +08:00
if currentTree . ID == docID {
2022-09-23 19:42:27 +08:00
// 同文档块引转脚注缩略定义 https://github.com/siyuan-note/siyuan/issues/3299
2022-05-26 15:18:53 +08:00
if text := sql . GetRefText ( n . ID ) ; 64 < utf8 . RuneCountInString ( text ) {
var unlinkChildren [ ] * ast . Node
for c := n . FirstChild ; nil != c ; c = c . Next {
unlinkChildren = append ( unlinkChildren , c )
}
for _ , c := range unlinkChildren {
c . Unlink ( )
}
text = gulu . Str . SubStr ( text , 64 ) + "..."
n . AppendChild ( & ast . Node { Type : ast . NodeText , Tokens : [ ] byte ( text ) } )
return ast . WalkSkipChildren
}
}
return ast . WalkContinue
} )
footnotesDef . AppendChild ( node )
}
footnotesDefBlock . AppendChild ( footnotesDef )
}
return
}
2024-12-01 12:25:00 +08:00
func blockLink2Ref ( currentTree * parse . Tree , id string , treeCache * map [ string ] * parse . Tree , depth * int ) {
* depth ++
if 4096 < * depth {
return
}
b := treenode . GetBlockTree ( id )
if nil == b {
return
}
2025-01-16 17:23:02 +08:00
t , err := loadTreeWithCache ( b . RootID , treeCache )
2024-12-18 01:14:47 +08:00
if nil != err {
return
2024-12-01 12:25:00 +08:00
}
2024-12-18 01:14:47 +08:00
2024-12-01 12:25:00 +08:00
node := treenode . GetNodeInTree ( t , b . ID )
if nil == node {
logging . LogErrorf ( "not found node [%s] in tree [%s]" , b . ID , t . Root . ID )
return
}
blockLink2Ref0 ( currentTree , node , treeCache , depth )
if ast . NodeHeading == node . Type {
children := treenode . HeadingChildren ( node )
for _ , c := range children {
blockLink2Ref0 ( currentTree , c , treeCache , depth )
}
}
return
}
func blockLink2Ref0 ( currentTree * parse . Tree , node * ast . Node , treeCache * map [ string ] * parse . Tree , depth * int ) {
ast . Walk ( node , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if treenode . IsBlockLink ( n ) {
n . TextMarkType = "block-ref"
n . TextMarkBlockRefID = strings . TrimPrefix ( n . TextMarkAHref , "siyuan://blocks/" )
n . TextMarkBlockRefSubtype = "s"
blockLink2Ref ( currentTree , n . TextMarkBlockRefID , treeCache , depth )
return ast . WalkSkipChildren
} else if treenode . IsBlockRef ( n ) {
defID , _ , _ := treenode . GetBlockRef ( n )
blockLink2Ref ( currentTree , defID , treeCache , depth )
}
return ast . WalkContinue
} )
}
2024-11-28 09:42:31 +08:00
func collectFootnotesDefs ( currentTree * parse . Tree , id string , refFootnotes * [ ] * refAsFootnotes , treeCache * map [ string ] * parse . Tree , depth * int ) {
2022-05-26 15:18:53 +08:00
* depth ++
if 4096 < * depth {
return
}
b := treenode . GetBlockTree ( id )
if nil == b {
return
}
2025-01-16 17:23:02 +08:00
t , err := loadTreeWithCache ( b . RootID , treeCache )
2024-12-18 01:14:47 +08:00
if nil != err {
return
2022-05-26 15:18:53 +08:00
}
2024-12-18 01:14:47 +08:00
2022-05-26 15:18:53 +08:00
node := treenode . GetNodeInTree ( t , b . ID )
if nil == node {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "not found node [%s] in tree [%s]" , b . ID , t . Root . ID )
2022-05-26 15:18:53 +08:00
return
}
2024-11-28 09:42:31 +08:00
collectFootnotesDefs0 ( currentTree , node , refFootnotes , treeCache , depth )
2022-05-26 15:18:53 +08:00
if ast . NodeHeading == node . Type {
children := treenode . HeadingChildren ( node )
for _ , c := range children {
2024-11-28 09:42:31 +08:00
collectFootnotesDefs0 ( currentTree , c , refFootnotes , treeCache , depth )
2022-05-26 15:18:53 +08:00
}
}
return
}
2024-11-28 09:42:31 +08:00
func collectFootnotesDefs0 ( currentTree * parse . Tree , node * ast . Node , refFootnotes * [ ] * refAsFootnotes , treeCache * map [ string ] * parse . Tree , depth * int ) {
2022-05-26 15:18:53 +08:00
ast . Walk ( node , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
2022-09-16 18:02:04 +08:00
if treenode . IsBlockRef ( n ) {
2022-11-16 21:41:24 +08:00
defID , refText , _ := treenode . GetBlockRef ( n )
2022-05-26 15:18:53 +08:00
if nil == getRefAsFootnotes ( defID , refFootnotes ) {
2024-11-28 09:42:31 +08:00
if isNodeInTree ( defID , currentTree ) {
// 当前文档内不转换脚注,直接使用锚点哈希 https://github.com/siyuan-note/siyuan/issues/13283
return ast . WalkSkipChildren
}
2022-11-16 21:41:24 +08:00
anchorText := refText
2022-05-26 15:18:53 +08:00
if Conf . Editor . BlockRefDynamicAnchorTextMaxLen < utf8 . RuneCountInString ( anchorText ) {
anchorText = gulu . Str . SubStr ( anchorText , Conf . Editor . BlockRefDynamicAnchorTextMaxLen ) + "..."
}
* refFootnotes = append ( * refFootnotes , & refAsFootnotes {
defID : defID ,
refNum : strconv . Itoa ( len ( * refFootnotes ) + 1 ) ,
refAnchorText : anchorText ,
} )
2024-11-28 09:42:31 +08:00
collectFootnotesDefs ( currentTree , defID , refFootnotes , treeCache , depth )
2022-05-26 15:18:53 +08:00
}
return ast . WalkSkipChildren
}
return ast . WalkContinue
} )
}
2024-11-28 09:42:31 +08:00
func isNodeInTree ( id string , tree * parse . Tree ) ( ret bool ) {
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if n . ID == id {
ret = true
return ast . WalkStop
}
return ast . WalkContinue
} )
return
}
2022-05-26 15:18:53 +08:00
func getRefAsFootnotes ( defID string , slice * [ ] * refAsFootnotes ) * refAsFootnotes {
for _ , e := range * slice {
if e . defID == defID {
return e
}
}
return nil
}
type refAsFootnotes struct {
defID string
refNum string
refAnchorText string
}
2024-06-24 23:11:41 +08:00
func processFileAnnotationRef ( refID string , n * ast . Node , fileAnnotationRefMode int ) ast . WalkStatus {
2022-09-16 22:59:24 +08:00
p := refID [ : strings . LastIndex ( refID , "/" ) ]
absPath , err := GetAssetAbsPath ( p )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-09-16 22:59:24 +08:00
logging . LogWarnf ( "get assets abs path by rel path [%s] failed: %s" , p , err )
return ast . WalkSkipChildren
}
sya := absPath + ".sya"
syaData , err := os . ReadFile ( sya )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-09-16 22:59:24 +08:00
logging . LogErrorf ( "read file [%s] failed: %s" , sya , err )
return ast . WalkSkipChildren
}
syaJSON := map [ string ] interface { } { }
2024-09-04 04:40:50 +03:00
if err = gulu . JSON . UnmarshalJSON ( syaData , & syaJSON ) ; err != nil {
2022-09-16 22:59:24 +08:00
logging . LogErrorf ( "unmarshal file [%s] failed: %s" , sya , err )
return ast . WalkSkipChildren
}
annotationID := refID [ strings . LastIndex ( refID , "/" ) + 1 : ]
annotationData := syaJSON [ annotationID ]
if nil == annotationData {
logging . LogErrorf ( "not found annotation [%s] in .sya" , annotationID )
return ast . WalkSkipChildren
}
pages := annotationData . ( map [ string ] interface { } ) [ "pages" ] . ( [ ] interface { } )
page := int ( pages [ 0 ] . ( map [ string ] interface { } ) [ "index" ] . ( float64 ) ) + 1
pageStr := strconv . Itoa ( page )
2022-12-08 20:32:42 +08:00
refText := n . TextMarkTextContent
2022-09-16 22:59:24 +08:00
ext := filepath . Ext ( p )
file := p [ 7 : len ( p ) - 23 - len ( ext ) ] + ext
fileAnnotationRefLink := & ast . Node { Type : ast . NodeLink }
fileAnnotationRefLink . AppendChild ( & ast . Node { Type : ast . NodeOpenBracket } )
2023-01-02 23:26:01 +08:00
if 0 == fileAnnotationRefMode {
2022-09-16 22:59:24 +08:00
fileAnnotationRefLink . AppendChild ( & ast . Node { Type : ast . NodeLinkText , Tokens : [ ] byte ( file + " - p" + pageStr + " - " + refText ) } )
} else {
fileAnnotationRefLink . AppendChild ( & ast . Node { Type : ast . NodeLinkText , Tokens : [ ] byte ( refText ) } )
}
fileAnnotationRefLink . AppendChild ( & ast . Node { Type : ast . NodeCloseBracket } )
fileAnnotationRefLink . AppendChild ( & ast . Node { Type : ast . NodeOpenParen } )
2024-06-24 23:11:41 +08:00
dest := p + "#page=" + pageStr // https://github.com/siyuan-note/siyuan/issues/11780
2024-06-21 19:46:45 +08:00
fileAnnotationRefLink . AppendChild ( & ast . Node { Type : ast . NodeLinkDest , Tokens : [ ] byte ( dest ) } )
2022-09-16 22:59:24 +08:00
fileAnnotationRefLink . AppendChild ( & ast . Node { Type : ast . NodeCloseParen } )
n . InsertBefore ( fileAnnotationRefLink )
return ast . WalkSkipChildren
}
2023-05-10 22:53:23 +08:00
2024-12-18 01:06:58 +08:00
func exportPandocConvertZip ( baseFolderName string , docPaths , defBlockIDs [ ] string ,
pandocFrom , pandocTo , ext string , treeCache * map [ string ] * parse . Tree ) ( zipPath string ) {
2024-12-16 22:50:55 +08:00
defer util . ClearPushProgress ( 100 )
2023-05-10 22:53:23 +08:00
dir , name := path . Split ( baseFolderName )
name = util . FilterFileName ( name )
if strings . HasSuffix ( name , ".." ) {
// 文档标题以 `..` 结尾时无法导出 Markdown https://github.com/siyuan-note/siyuan/issues/4698
// 似乎是 os.MkdirAll 的 bug, 以 .. 结尾的路径无法创建,所以这里加上 _ 结尾
name += "_"
}
baseFolderName = path . Join ( dir , name )
exportFolder := filepath . Join ( util . TempDir , "export" , baseFolderName + ext )
2023-05-11 09:21:32 +08:00
os . RemoveAll ( exportFolder )
2024-09-04 04:40:50 +03:00
if err := os . MkdirAll ( exportFolder , 0755 ) ; err != nil {
2023-05-10 22:53:23 +08:00
logging . LogErrorf ( "create export temp folder failed: %s" , err )
return
}
2024-01-28 10:35:09 +08:00
exportRefMode := Conf . Export . BlockRefMode
2024-12-16 22:50:55 +08:00
wrotePathHash := map [ string ] string { }
2024-12-16 23:06:01 +08:00
assetsPathMap , err := allAssetAbsPaths ( )
if nil != err {
logging . LogWarnf ( "get assets abs path failed: %s" , err )
return
}
2024-12-16 22:50:55 +08:00
2023-05-10 22:53:23 +08:00
luteEngine := util . NewLute ( )
2024-12-16 22:50:55 +08:00
for i , p := range docPaths {
2024-12-09 12:11:26 +08:00
id := util . GetTreeID ( p )
2024-12-18 17:17:24 +08:00
hPath , md := exportMarkdownContent ( id , ext , exportRefMode , defBlockIDs , false , treeCache )
2023-05-10 22:53:23 +08:00
dir , name = path . Split ( hPath )
dir = util . FilterFilePath ( dir ) // 导出文档时未移除不支持的文件名符号 https://github.com/siyuan-note/siyuan/issues/4590
name = util . FilterFileName ( name )
hPath = path . Join ( dir , name )
p = hPath + ext
writePath := filepath . Join ( exportFolder , p )
2024-12-16 22:50:55 +08:00
hash := fmt . Sprintf ( "%x" , sha1 . Sum ( [ ] byte ( md ) ) )
if gulu . File . IsExist ( writePath ) && hash != wrotePathHash [ writePath ] {
2023-05-10 22:53:23 +08:00
// 重名文档加 ID
p = hPath + "-" + id + ext
writePath = filepath . Join ( exportFolder , p )
}
writeFolder := filepath . Dir ( writePath )
2024-09-04 04:40:50 +03:00
if err := os . MkdirAll ( writeFolder , 0755 ) ; err != nil {
2023-05-10 22:53:23 +08:00
logging . LogErrorf ( "create export temp folder [%s] failed: %s" , writeFolder , err )
continue
}
// 解析导出后的标准 Markdown, 汇总 assets
tree := parse . Parse ( "" , gulu . Str . ToBytes ( md ) , luteEngine . ParseOptions )
var assets [ ] string
assets = append ( assets , assetsLinkDestsInTree ( tree ) ... )
for _ , asset := range assets {
asset = string ( html . DecodeDestination ( [ ] byte ( asset ) ) )
if strings . Contains ( asset , "?" ) {
asset = asset [ : strings . LastIndex ( asset , "?" ) ]
}
2024-01-28 00:23:47 +08:00
if ! strings . HasPrefix ( asset , "assets/" ) {
continue
}
2024-12-16 23:06:01 +08:00
srcPath := assetsPathMap [ asset ]
if "" == srcPath {
logging . LogWarnf ( "get asset [%s] abs path failed" , asset )
2023-05-10 22:53:23 +08:00
continue
}
destPath := filepath . Join ( writeFolder , asset )
2024-12-16 23:06:01 +08:00
if copyErr := filelock . Copy ( srcPath , destPath ) ; copyErr != nil {
2023-05-10 22:53:23 +08:00
logging . LogErrorf ( "copy asset from [%s] to [%s] failed: %s" , srcPath , destPath , err )
continue
2023-05-17 15:37:39 +08:00
}
}
// 调用 Pandoc 进行格式转换
2025-04-20 17:27:14 +08:00
pandocErr := util . Pandoc ( pandocFrom , pandocTo , writePath , md )
if pandocErr != nil {
logging . LogErrorf ( "pandoc failed: %s" , pandocErr )
2023-05-17 15:37:39 +08:00
continue
}
2024-12-16 22:50:55 +08:00
wrotePathHash [ writePath ] = hash
util . PushEndlessProgress ( Conf . language ( 65 ) + " " + fmt . Sprintf ( Conf . language ( 70 ) , fmt . Sprintf ( "%d/%d %s" , i + 1 , len ( docPaths ) , name ) ) )
2023-05-10 22:53:23 +08:00
}
zipPath = exportFolder + ".zip"
zip , err := gulu . Zip . Create ( zipPath )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-05-10 22:53:23 +08:00
logging . LogErrorf ( "create export markdown zip [%s] failed: %s" , exportFolder , err )
return ""
}
// 导出 Markdown zip 包内不带文件夹 https://github.com/siyuan-note/siyuan/issues/6869
entries , err := os . ReadDir ( exportFolder )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-05-10 22:53:23 +08:00
logging . LogErrorf ( "read export markdown folder [%s] failed: %s" , exportFolder , err )
return ""
}
2025-01-01 11:02:16 +08:00
zipCallback := func ( filename string ) {
util . PushEndlessProgress ( Conf . language ( 65 ) + " " + fmt . Sprintf ( Conf . language ( 253 ) , filename ) )
}
2023-05-10 22:53:23 +08:00
for _ , entry := range entries {
2025-01-01 11:02:16 +08:00
entryName := entry . Name ( )
entryPath := filepath . Join ( exportFolder , entryName )
2023-05-10 22:53:23 +08:00
if gulu . File . IsDir ( entryPath ) {
2025-01-01 11:02:16 +08:00
err = zip . AddDirectory ( entryName , entryPath , zipCallback )
2023-05-10 22:53:23 +08:00
} else {
2025-01-01 11:02:16 +08:00
err = zip . AddEntry ( entryName , entryPath , zipCallback )
2023-05-10 22:53:23 +08:00
}
2024-09-04 04:40:50 +03:00
if err != nil {
2025-01-01 11:02:16 +08:00
logging . LogErrorf ( "add entry [%s] to zip failed: %s" , entryName , err )
2023-05-10 22:53:23 +08:00
return ""
}
}
2024-09-04 04:40:50 +03:00
if err = zip . Close ( ) ; err != nil {
2023-05-10 22:53:23 +08:00
logging . LogErrorf ( "close export markdown zip failed: %s" , err )
}
os . RemoveAll ( exportFolder )
zipPath = "/export/" + url . PathEscape ( filepath . Base ( zipPath ) )
return
}
2024-01-28 00:23:47 +08:00
func getExportBlockRefLinkText ( blockRef * ast . Node , blockRefTextLeft , blockRefTextRight string ) ( defID , linkText string ) {
defID , linkText , _ = treenode . GetBlockRef ( blockRef )
if "" == linkText {
linkText = sql . GetRefText ( defID )
}
2024-11-17 11:34:38 +08:00
linkText = util . UnescapeHTML ( linkText ) // 块引锚文本导出时 `&` 变为实体 `&` https://github.com/siyuan-note/siyuan/issues/7659
2024-01-28 00:23:47 +08:00
if Conf . Editor . BlockRefDynamicAnchorTextMaxLen < utf8 . RuneCountInString ( linkText ) {
linkText = gulu . Str . SubStr ( linkText , Conf . Editor . BlockRefDynamicAnchorTextMaxLen ) + "..."
}
linkText = blockRefTextLeft + linkText + blockRefTextRight
return
}
2024-12-18 01:06:58 +08:00
func prepareExportTrees ( docPaths [ ] string ) ( defBlockIDs [ ] string , trees * map [ string ] * parse . Tree , relatedDocPaths [ ] string ) {
trees = & map [ string ] * parse . Tree { }
treeCache := & map [ string ] * parse . Tree { }
defBlockIDs = [ ] string { }
2025-01-01 11:02:16 +08:00
for i , p := range docPaths {
2025-01-16 17:23:02 +08:00
rootID := strings . TrimSuffix ( path . Base ( p ) , ".sy" )
if ! ast . IsNodeIDPattern ( rootID ) {
2024-12-18 17:17:24 +08:00
continue
}
2025-01-16 17:23:02 +08:00
tree , err := loadTreeWithCache ( rootID , treeCache )
2024-12-18 01:06:58 +08:00
if err != nil {
continue
}
exportRefTrees ( tree , & defBlockIDs , trees , treeCache )
2025-01-01 11:02:16 +08:00
util . PushEndlessProgress ( Conf . language ( 65 ) + " " + fmt . Sprintf ( Conf . language ( 70 ) , fmt . Sprintf ( "%d/%d %s" , i + 1 , len ( docPaths ) , tree . Root . IALAttr ( "title" ) ) ) )
2024-12-18 01:06:58 +08:00
}
for _ , tree := range * trees {
relatedDocPaths = append ( relatedDocPaths , tree . Path )
}
relatedDocPaths = gulu . Str . RemoveDuplicatedElem ( relatedDocPaths )
return
}
func exportRefTrees ( tree * parse . Tree , defBlockIDs * [ ] string , retTrees , treeCache * map [ string ] * parse . Tree ) {
if nil != ( * retTrees ) [ tree . ID ] {
return
}
( * retTrees ) [ tree . ID ] = tree
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if treenode . IsBlockRef ( n ) {
defID , _ , _ := treenode . GetBlockRef ( n )
if "" == defID {
return ast . WalkContinue
}
defBlock := treenode . GetBlockTree ( defID )
if nil == defBlock {
return ast . WalkSkipChildren
}
var defTree * parse . Tree
var err error
if ( * treeCache ) [ defBlock . RootID ] != nil {
defTree = ( * treeCache ) [ defBlock . RootID ]
} else {
2024-12-18 01:14:47 +08:00
defTree , err = loadTreeWithCache ( defBlock . RootID , treeCache )
2024-12-18 01:06:58 +08:00
if err != nil {
return ast . WalkSkipChildren
}
( * treeCache ) [ defBlock . RootID ] = defTree
}
* defBlockIDs = append ( * defBlockIDs , defID )
exportRefTrees ( defTree , defBlockIDs , retTrees , treeCache )
} else if treenode . IsBlockLink ( n ) {
defID := strings . TrimPrefix ( n . TextMarkAHref , "siyuan://blocks/" )
if "" == defID {
return ast . WalkContinue
}
defBlock := treenode . GetBlockTree ( defID )
if nil == defBlock {
return ast . WalkSkipChildren
}
var defTree * parse . Tree
var err error
if ( * treeCache ) [ defBlock . RootID ] != nil {
defTree = ( * treeCache ) [ defBlock . RootID ]
} else {
2024-12-18 01:14:47 +08:00
defTree , err = loadTreeWithCache ( defBlock . RootID , treeCache )
2024-12-18 01:06:58 +08:00
if err != nil {
return ast . WalkSkipChildren
}
( * treeCache ) [ defBlock . RootID ] = defTree
}
* defBlockIDs = append ( * defBlockIDs , defID )
exportRefTrees ( defTree , defBlockIDs , retTrees , treeCache )
} else if ast . NodeAttributeView == n . Type {
// 导出数据库所在文档时一并导出绑定块所在文档
// Export the binding block docs when exporting the doc where the database is located https://github.com/siyuan-note/siyuan/issues/11486
avID := n . AttributeViewID
if "" == avID {
return ast . WalkContinue
}
attrView , _ := av . ParseAttributeView ( avID )
if nil == attrView {
return ast . WalkContinue
}
blockKeyValues := attrView . GetBlockKeyValues ( )
if nil == blockKeyValues {
return ast . WalkContinue
}
for _ , val := range blockKeyValues . Values {
defBlock := treenode . GetBlockTree ( val . BlockID )
if nil == defBlock {
continue
}
var defTree * parse . Tree
var err error
if ( * treeCache ) [ defBlock . RootID ] != nil {
defTree = ( * treeCache ) [ defBlock . RootID ]
} else {
2024-12-18 01:14:47 +08:00
defTree , err = loadTreeWithCache ( defBlock . RootID , treeCache )
2024-12-18 01:06:58 +08:00
if err != nil {
continue
}
( * treeCache ) [ defBlock . RootID ] = defTree
}
* defBlockIDs = append ( * defBlockIDs , val . BlockID )
exportRefTrees ( defTree , defBlockIDs , retTrees , treeCache )
}
}
return ast . WalkContinue
} )
* defBlockIDs = gulu . Str . RemoveDuplicatedElem ( * defBlockIDs )
}
func loadTreeWithCache ( id string , treeCache * map [ string ] * parse . Tree ) ( tree * parse . Tree , err error ) {
if tree = ( * treeCache ) [ id ] ; nil != tree {
return
}
tree , err = LoadTreeByBlockID ( id )
if nil == err && nil != tree {
( * treeCache ) [ id ] = tree
}
return
}