Something went wrong. Try again.
A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804// SiYuan - Refactor your thinking// 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" "crypto/sha1" "encoding/csv" "errors" "fmt" "net/http" "net/url" "os" "os/exec" "path" "path/filepath" "sort" "strconv" "strings" "time" "unicode/utf8"
"github.com/88250/go-humanize" "github.com/88250/gulu" "github.com/88250/lute/ast" "github.com/88250/lute/editor" "github.com/88250/lute/html" "github.com/88250/lute/lex" "github.com/88250/lute/parse" "github.com/88250/lute/render" "github.com/emirpasic/gods/sets/hashset" "github.com/emirpasic/gods/stacks/linkedliststack" "github.com/imroc/req/v3" shellquote "github.com/kballard/go-shellquote" "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" "github.com/siyuan-note/filelock" "github.com/siyuan-note/httpclient" "github.com/siyuan-note/logging" "github.com/siyuan-note/riff" "github.com/siyuan-note/siyuan/kernel/av" "github.com/siyuan-note/siyuan/kernel/filesys" "github.com/siyuan-note/siyuan/kernel/sql" "github.com/siyuan-note/siyuan/kernel/treenode" "github.com/siyuan-note/siyuan/kernel/util")
func ExportCodeBlock(blockID string) (filePath string, err error) { // Supports exporting a code block as a file https://github.com/siyuan-note/siyuan/pull/16774
tree, _ := LoadTreeByBlockID(blockID) if nil == tree { err = ErrBlockNotFound return }
node := treenode.GetNodeInTree(tree, blockID) if nil == node { err = ErrBlockNotFound return }
if ast.NodeCodeBlock != node.Type { err = errors.New("not a code block") return }
code := node.ChildByType(ast.NodeCodeBlockCode) if nil == code { err = errors.New("code block has no code node") return }
name := tree.Root.IALAttr("title") + "-" + util.CurrentTimeSecondsStr() + ".txt" name = util.FilterFileName(name) exportFolder := filepath.Join(util.TempDir, "export", "code") if err = os.MkdirAll(exportFolder, 0755); err != nil { logging.LogErrorf("create export temp folder failed: %s", err) return }
code.Tokens = bytes.ReplaceAll(code.Tokens, []byte(editor.Zwj+"```"), []byte("```"))
writePath := filepath.Join(exportFolder, name) err = filelock.WriteFile(writePath, code.Tokens) if nil != err { return }
filePath = "/export/code/" + url.PathEscape(name) return}
func ExportAv2CSV(avID, blockID string) (zipPath string, err error) { // Database block supports export as CSV https://github.com/siyuan-note/siyuan/issues/10072
attrView, err := av.ParseAttributeView(avID) if err != nil { return }
node, _, err := getNodeByBlockID(nil, blockID) if nil == node { return } viewID := node.IALAttr(av.NodeAttrView) view, err := attrView.GetCurrentView(viewID) if err != nil { return }
name := util.FilterFileName(getAttrViewName(attrView)) table := getAttrViewTable(attrView, view, "")
// 遵循视图过滤和排序规则 Use filtering and sorting of current view settings when exporting database blocks https://github.com/siyuan-note/siyuan/issues/10474 cachedAttrViews := map[string]*av.AttributeView{} rollupFurtherCollections := sql.GetFurtherCollections(attrView, cachedAttrViews) av.Filter(table, attrView, rollupFurtherCollections, cachedAttrViews) av.Sort(table, attrView)
exportFolder := filepath.Join(util.TempDir, "export", "csv", name) if err = os.MkdirAll(exportFolder, 0755); err != nil { logging.LogErrorf("mkdir [%s] failed: %s", exportFolder, err) return } csvPath := filepath.Join(exportFolder, name+".csv")
f, err := os.OpenFile(csvPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { logging.LogErrorf("open [%s] failed: %s", csvPath, err) return }
if _, err = f.WriteString("\xEF\xBB\xBF"); err != nil { // 写入 UTF-8 BOM,避免使用 Microsoft Excel 打开乱码 logging.LogErrorf("write UTF-8 BOM to [%s] failed: %s", csvPath, err) f.Close() return }
writer := csv.NewWriter(f) var header []string for _, col := range table.Columns { header = append(header, col.Name) } if err = writer.Write(header); err != nil { logging.LogErrorf("write csv header [%s] failed: %s", header, err) f.Close() return }
var assets []string rowNum := 1 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 { cell.Value.Date = av.NewFormattedValueDate(cell.Value.Date.Content, cell.Value.Date.Content2, av.DateFormatNone, cell.Value.Date.IsNotTime, cell.Value.Date.HasEndDate) } } else if av.KeyTypeCreated == cell.Value.Type { if nil != cell.Value.Created { key, _ := attrView.GetKey(cell.Value.KeyID) isNotTime := false if nil != key && nil != key.Created { isNotTime = !key.Created.IncludeTime }
cell.Value.Created = av.NewFormattedValueCreated(cell.Value.Created.Content, 0, av.CreatedFormatNone, isNotTime) } } else if av.KeyTypeUpdated == cell.Value.Type { if nil != cell.Value.Updated { key, _ := attrView.GetKey(cell.Value.KeyID) isNotTime := false if nil != key && nil != key.Updated { isNotTime = !key.Updated.IncludeTime }
cell.Value.Updated = av.NewFormattedValueUpdated(cell.Value.Updated.Content, 0, av.UpdatedFormatNone, isNotTime) } } else if av.KeyTypeMAsset == cell.Value.Type { if nil != cell.Value.MAsset { buf := &bytes.Buffer{} for _, a := range cell.Value.MAsset { if av.AssetTypeImage == a.Type { buf.WriteString(" buf.WriteString(a.Content) buf.WriteString(") ") if util.IsAssetLinkDest([]byte(a.Content), true) { assets = append(assets, a.Content) } } else if av.AssetTypeFile == a.Type { buf.WriteString("[") buf.WriteString(a.Name) buf.WriteString("](") buf.WriteString(a.Content) buf.WriteString(") ") if util.IsAssetLinkDest([]byte(a.Content), true) { assets = append(assets, a.Content) } } else { buf.WriteString(a.Content) buf.WriteString(" ") } } val = strings.TrimSpace(buf.String()) } } else if av.KeyTypeLineNumber == cell.Value.Type { val = strconv.Itoa(rowNum) } else if av.KeyTypeRollup == cell.Value.Type { for _, content := range cell.Value.Rollup.Contents { if av.KeyTypeMAsset == content.Type { buf := &bytes.Buffer{} for _, a := range content.MAsset { if av.AssetTypeImage == a.Type { buf.WriteString(" buf.WriteString(a.Content) buf.WriteString(") ") if util.IsAssetLinkDest([]byte(a.Content), true) { assets = append(assets, a.Content) } } else if av.AssetTypeFile == a.Type { buf.WriteString("[") buf.WriteString(a.Name) buf.WriteString("](") buf.WriteString(a.Content) buf.WriteString(") ") if util.IsAssetLinkDest([]byte(a.Content), true) { assets = append(assets, a.Content) } } else { buf.WriteString(a.Content) buf.WriteString(" ") } } val = strings.TrimSpace(buf.String()) } } }
if "" == val { val = cell.Value.String(true) } }
rowVal = append(rowVal, val) } if err = writer.Write(rowVal); err != nil { logging.LogErrorf("write csv row [%s] failed: %s", rowVal, err) f.Close() return } rowNum++ } writer.Flush()
for _, asset := range assets { srcAbsPath, getErr := GetAssetAbsPath(asset) if getErr != nil { logging.LogWarnf("resolve path of asset [%s] failed: %s", asset, getErr) continue } targetAbsPath := filepath.Join(exportFolder, asset) if copyErr := filelock.Copy(srcAbsPath, targetAbsPath); copyErr != nil { logging.LogWarnf("copy asset from [%s] to [%s] failed: %s", srcAbsPath, targetAbsPath, copyErr) } }
zipPath = exportFolder + ".db.zip" zip, err := gulu.Zip.Create(zipPath) if err != nil { logging.LogErrorf("create export .db.zip [%s] failed: %s", exportFolder, err) f.Close() return }
if err = zip.AddDirectory("", exportFolder); err != nil { logging.LogErrorf("create export .db.zip [%s] failed: %s", exportFolder, err) f.Close() return }
if err = zip.Close(); err != nil { 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) } zipPath = "/export/csv/" + url.PathEscape(filepath.Base(zipPath)) return}
func Export2Liandi(id string) (err error) { tree, err := LoadTreeByBlockID(id) if err != nil { logging.LogErrorf("load tree by block id [%s] failed: %s", id, err) return }
if IsUserGuide(tree.Box) { // Doc in the user guide no longer supports one-click sending to the community https://github.com/siyuan-note/siyuan/issues/8388 return errors.New(Conf.Language(204)) }
assets := getAssetsLinkDests(tree.Root, false) embedAssets := getQueryEmbedNodesAssetsLinkDests(tree.Root) assets = append(assets, embedAssets...) assets = gulu.Str.RemoveDuplicatedElem(assets) _, err = uploadAssets2Cloud(assets, bizTypeExport2Liandi, false) if err != nil { return }
msgId := util.PushMsg(Conf.Language(182), 15000) defer util.PushClearMsg(msgId)
// 判断帖子是否已经存在,存在则使用更新接口 const liandiArticleIdAttrName = "custom-liandi-articleid" const liandiArticleIdAttrNameOld = "custom-liandi-articleId" // 兼容旧属性名 foundArticle := false // 优先使用新属性名,如果不存在则尝试旧属性名 articleId := tree.Root.IALAttr(liandiArticleIdAttrName) if "" == articleId { articleId = tree.Root.IALAttr(liandiArticleIdAttrNameOld) } if "" != articleId { result := gulu.Ret.NewResult() request := httpclient.NewCloudRequest30s() resp, getErr := request. SetSuccessResult(result). SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}). Get(util.GetCloudAccountServer() + "/api/v2/article/update/" + articleId) if nil != getErr { logging.LogErrorf("get liandi article info failed: %s", getErr) return getErr }
switch resp.StatusCode { case 200: if 0 == result.Code { foundArticle = true } else if 1 == result.Code { foundArticle = false } case 404: foundArticle = false default: err = errors.New(fmt.Sprintf("get liandi article info failed [sc=%d]", resp.StatusCode)) return } }
apiURL := util.GetCloudAccountServer() + "/api/v2/article" if foundArticle { apiURL += "/" + articleId }
title := path.Base(tree.HPath) tags := tree.Root.IALAttr("tags") content := exportMarkdownContent0(id, tree, util.GetCloudForumAssetsServer()+time.Now().Format("2006/01")+"/siyuan/"+Conf.GetUser().UserId+"/", true, false, false, ".md", 3, 1, 1, "#", "#", "", "", false, false, nil, true, false) result := gulu.Ret.NewResult() request := httpclient.NewCloudRequest30s() request = request. SetSuccessResult(result). SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}). SetBody(map[string]interface{}{ "articleTitle": title, "articleTags": tags, "articleContent": content}) 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) }
if 0 != result.Code { msg := fmt.Sprintf("send article to liandi failed [code=%d, msg=%s]", result.Code, result.Msg) logging.LogErrorf(msg) util.PushClearMsg(msgId) return errors.New(result.Msg) }
if !foundArticle { articleId = result.Data.(string) tree, _ = LoadTreeByBlockID(id) // 这里必须重新加载,因为前面导出时已经修改了树结构 tree.Root.SetIALAttr(liandiArticleIdAttrName, articleId) if err = writeTreeUpsertQueue(tree); err != nil { return } }
util.PushMsg(fmt.Sprintf(Conf.Language(181), util.GetCloudAccountServer()+"/article/"+articleId), 7000) return}
func ExportSystemLog() (zipPath string) { exportFolder := filepath.Join(util.TempDir, "export", "system-log") os.RemoveAll(exportFolder) if err := os.MkdirAll(exportFolder, 0755); err != nil { logging.LogErrorf("create export temp folder failed: %s", err) return }
appLog := filepath.Join(util.HomeDir, ".config", "siyuan", "app.log") if gulu.File.IsExist(appLog) { to := filepath.Join(exportFolder, "app.log") if err := filelock.Copy(appLog, to); err != nil { logging.LogErrorf("copy app log from [%s] to [%s] failed: %s", err, appLog, to) } }
kernelLog := filepath.Join(util.HomeDir, ".config", "siyuan", "kernel.log") if gulu.File.IsExist(kernelLog) { to := filepath.Join(exportFolder, "kernel.log") if err := filelock.Copy(kernelLog, to); err != nil { logging.LogErrorf("copy kernel log from [%s] to [%s] failed: %s", err, kernelLog, to) } }
siyuanLog := filepath.Join(util.TempDir, "siyuan.log") if gulu.File.IsExist(siyuanLog) { to := filepath.Join(exportFolder, "siyuan.log") if err := filelock.Copy(siyuanLog, to); err != nil { logging.LogErrorf("copy kernel log from [%s] to [%s] failed: %s", err, siyuanLog, to) } }
mobileLog := filepath.Join(util.TempDir, "mobile.log") if gulu.File.IsExist(mobileLog) { to := filepath.Join(exportFolder, "mobile.log") if err := filelock.Copy(mobileLog, to); err != nil { logging.LogErrorf("copy mobile log from [%s] to [%s] failed: %s", err, mobileLog, to) } }
zipPath = exportFolder + ".zip" zip, err := gulu.Zip.Create(zipPath) if err != nil { logging.LogErrorf("create export log zip [%s] failed: %s", exportFolder, err) return "" }
if err = zip.AddDirectory("log", exportFolder); err != nil { logging.LogErrorf("create export log zip [%s] failed: %s", exportFolder, err) return "" }
if err = zip.Close(); err != nil { logging.LogErrorf("close export log zip failed: %s", err) }
os.RemoveAll(exportFolder) zipPath = "/export/" + url.PathEscape(filepath.Base(zipPath)) return}
func ExportNotebookSY(id string) (zipPath string) { zipPath = exportBoxSYZip(id) return}
func ExportSYs(ids []string) (zipPath string) { block := treenode.GetBlockTree(ids[0]) box := Conf.Box(block.BoxID) baseFolderName := path.Base(block.HPath) if "." == baseFolderName { baseFolderName = path.Base(block.Path) }
var docPaths []string bts := treenode.GetBlockTrees(ids) for _, bt := range bts { docPaths = append(docPaths, bt.Path)
if Conf.Export.IncludeSubDocs { docFiles := box.ListFiles(strings.TrimSuffix(bt.Path, ".sy")) for _, docFile := range docFiles { docPaths = append(docPaths, docFile.path) } } } zipPath = exportSYZip(block.BoxID, path.Dir(block.Path), baseFolderName, docPaths) return}
func ExportDataInFolder(exportFolder string) (name string, err error) { util.PushEndlessProgress(Conf.Language(65)) defer util.ClearPushProgress(100)
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 } }
zipPath, err := ExportData() if err != nil { return } name = filepath.Base(zipPath) name, err = url.PathUnescape(name) if err != nil { logging.LogErrorf("url unescape [%s] failed: %s", name, err) return }
util.PushEndlessProgress(Conf.Language(65)) defer util.ClearPushProgress(100)
targetZipPath := filepath.Join(exportFolder, name) zipAbsPath := filepath.Join(util.TempDir, "export", name) err = filelock.Copy(zipAbsPath, targetZipPath) if err != nil { 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) } return}
func ExportData() (zipPath string, err error) { util.PushEndlessProgress(Conf.Language(65)) defer util.ClearPushProgress(100)
name := util.FilterFileName(util.WorkspaceName) + "-" + util.CurrentTimeSecondsStr() exportFolder := filepath.Join(util.TempDir, "export", name) zipPath, err = exportData(exportFolder) if err != nil { return } zipPath = "/export/" + url.PathEscape(filepath.Base(zipPath)) return}
func exportData(exportFolder string) (zipPath string, err error) { FlushTxQueue()
logging.LogInfof("exporting data...")
baseFolderName := "data-" + util.CurrentTimeSecondsStr() if err = os.MkdirAll(exportFolder, 0755); err != nil { logging.LogErrorf("create export temp folder failed: %s", err) return }
data := filepath.Join(util.WorkspaceDir, "data") if err = filelock.Copy(data, exportFolder); err != nil { logging.LogErrorf("copy data dir from [%s] to [%s] failed: %s", data, baseFolderName, err) err = errors.New(fmt.Sprintf(Conf.Language(14), err.Error())) return }
zipPath = exportFolder + ".zip" zip, err := gulu.Zip.Create(zipPath) if err != nil { logging.LogErrorf("create export data zip [%s] failed: %s", exportFolder, err) return }
zipCallback := func(filename string) { util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(253), filename)) }
if err = zip.AddDirectory(baseFolderName, exportFolder, zipCallback); err != nil { logging.LogErrorf("create export data zip [%s] failed: %s", exportFolder, err) return }
if err = zip.Close(); err != nil { logging.LogErrorf("close export data zip failed: %s", err) }
os.RemoveAll(exportFolder) logging.LogInfof("export data done [%s]", zipPath) return}
func ExportResources(resourcePaths []string, mainName string) (exportFilePath string, err error) { FlushTxQueue()
// 用于导出的临时文件夹完整路径 exportFolderPath := filepath.Join(util.TempDir, "export", mainName) if err = os.MkdirAll(exportFolderPath, 0755); err != nil { logging.LogErrorf("create export temp folder failed: %s", err) return }
// 将需要导出的文件/文件夹复制到临时文件夹 for _, resourcePath := range resourcePaths { 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 }
resourceBaseName := filepath.Base(resourceFullPath) // 资源名称 resourceCopyPath := filepath.Join(exportFolderPath, resourceBaseName) // 资源副本完整路径 if err = filelock.Copy(resourceFullPath, resourceCopyPath); err != nil { 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) if err != nil { logging.LogErrorf("create export zip [%s] failed: %s", zipFilePath, err) return }
if err = zip.AddDirectory(mainName, exportFolderPath); err != nil { logging.LogErrorf("create export zip [%s] failed: %s", exportFolderPath, err) return }
if err = zip.Close(); err != nil { logging.LogErrorf("close export zip failed: %s", err) }
os.RemoveAll(exportFolderPath)
exportFilePath = path.Join("temp", "export", mainName+".zip") // 导出的 *.zip 文件相对于工作区目录的路径 return}
func ExportPreview(id string, fillCSSVar bool) (retStdHTML string) { blockRefMode := Conf.Export.BlockRefMode bt := treenode.GetBlockTree(id) if nil == bt { return }
tree := prepareExportTree(bt) tree = exportTree(tree, false, false, true, blockRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode, "#", "#", // 这里固定使用 # 包裹标签,否则无法正确解析标签 https://github.com/siyuan-note/siyuan/issues/13857 Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight, Conf.Export.AddTitle, Conf.Export.InlineMemo, true, true) luteEngine := NewLute() enableLuteInlineSyntax(luteEngine) luteEngine.SetFootnotes(true) addBlockIALNodes(tree, false)
adjustHeadingLevel(bt, tree)
// 移除超级块的属性列表 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() }
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue }
if ast.NodeFootnotesRef == n.Type && nil != n.Next { // https://github.com/siyuan-note/siyuan/issues/15654 nextText := n.NextNodeText() if strings.HasPrefix(nextText, "(") && strings.HasSuffix(nextText, ")") { n.InsertAfter(&ast.Node{Type: ast.NodeText, Tokens: []byte(editor.Zwsp)}) } } return ast.WalkContinue })
md := treenode.FormatNode(tree.Root, luteEngine) tree = parse.Parse("", []byte(md), luteEngine.ParseOptions) // 使用实际主题样式值替换样式变量 Use real theme style value replace var in preview mode https://github.com/siyuan-note/siyuan/issues/11458 if fillCSSVar { fillThemeStyleVar(tree) } luteEngine.RenderOptions.ProtyleMarkNetImg = false retStdHTML = luteEngine.ProtylePreview(tree, luteEngine.RenderOptions, luteEngine.ParseOptions)
if footnotesDefBlock := tree.Root.ChildByType(ast.NodeFootnotesDefBlock); nil != footnotesDefBlock { footnotesDefBlock.Unlink() } return}
func ExportDocx(id, savePath string, removeAssets, merge bool) (fullPath string, err error) { if !util.IsValidPandocBin(Conf.Export.PandocBin) { Conf.Export.PandocBin = util.PandocBinPath Conf.Save() if !util.IsValidPandocBin(Conf.Export.PandocBin) { err = errors.New(Conf.Language(115)) return } }
tmpDir := filepath.Join(util.TempDir, "export", gulu.Rand.String(7)) if err = os.MkdirAll(tmpDir, 0755); err != nil { return } defer os.Remove(tmpDir) name, content := ExportMarkdownHTML(id, tmpDir, true, merge) content = strings.ReplaceAll(content, " \n", "<br>\n")
tmpDocxPath := filepath.Join(tmpDir, name+".docx") args := []string{ "-f", "html+tex_math_dollars", "--resource-path", tmpDir, "-o", tmpDocxPath, }
params := util.ReplaceNewline(Conf.Export.PandocParams, " ") if "" != params { customArgs, parseErr := shellquote.Split(params) if nil != parseErr { logging.LogErrorf("parse pandoc custom params [%s] failed: %s", params, parseErr) } else { args = append(args, customArgs...) } }
hasLuaFilter := false for i := 0; i < len(args)-1; i++ { if "--lua-filter" == args[i] { hasLuaFilter = true break } } if !hasLuaFilter { args = append(args, "--lua-filter", util.PandocColorFilterPath) }
hasReferenceDoc := false for i := 0; i < len(args)-1; i++ { if "--reference-doc" == args[i] { hasReferenceDoc = true break } } if !hasReferenceDoc { args = append(args, "--reference-doc", util.PandocTemplatePath) }
pandoc := exec.Command(Conf.Export.PandocBin, args...) gulu.CmdAttr(pandoc) pandoc.Stdin = bytes.NewBufferString(content) output, err := pandoc.CombinedOutput() if err != nil { argStr := strings.Join(args, " ") msg := gulu.DecodeCmdOutput(output) logging.LogErrorf("export docx [%s] failed: %s", argStr, msg) err = errors.New(fmt.Sprintf(Conf.Language(14), msg)) return }
fullPath = filepath.Join(savePath, name+".docx") fullPath = util.GetUniqueFilename(fullPath) if err = filelock.Copy(tmpDocxPath, fullPath); err != nil { logging.LogErrorf("export docx failed: %s", err) err = errors.New(fmt.Sprintf(Conf.Language(14), err)) return }
if tmpAssets := filepath.Join(tmpDir, "assets"); !removeAssets && gulu.File.IsDir(tmpAssets) { if err = filelock.Copy(tmpAssets, filepath.Join(savePath, "assets")); err != nil { logging.LogErrorf("export docx failed: %s", err) err = errors.New(fmt.Sprintf(Conf.Language(14), err)) return } } return}
func ExportMarkdownHTML(id, savePath string, docx, merge bool) (name, dom string) { bt := treenode.GetBlockTree(id) if nil == bt { return }
tree := prepareExportTree(bt)
if merge { var mergeErr error tree, mergeErr = mergeSubDocs(tree) if nil != mergeErr { logging.LogErrorf("merge sub docs failed: %s", mergeErr) return } }
blockRefMode := Conf.Export.BlockRefMode tree = exportTree(tree, true, false, true, blockRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode, Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker, Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight, Conf.Export.AddTitle, Conf.Export.InlineMemo, true, true) name = path.Base(tree.HPath) name = util.FilterFileName(name) // 导出 PDF、HTML 和 Word 时未移除不支持的文件名符号 https://github.com/siyuan-note/siyuan/issues/5614 savePath = strings.TrimSpace(savePath)
if err := os.MkdirAll(savePath, 0755); err != nil { logging.LogErrorf("mkdir [%s] failed: %s", savePath, err) return }
if docx { netAssets2LocalAssets0(tree, true, "", filepath.Join(savePath, "assets"), false) }
assets := getAssetsLinkDests(tree.Root, docx) for _, asset := range assets { if !util.IsAssetLinkDest([]byte(asset), docx) { continue }
if strings.Contains(asset, "?") { asset = asset[:strings.LastIndex(asset, "?")] }
srcAbsPath, err := GetAssetAbsPath(asset) if err != nil { logging.LogWarnf("resolve path of asset [%s] failed: %s", asset, err) continue } targetAbsPath := filepath.Join(savePath, asset) if err = filelock.Copy(srcAbsPath, targetAbsPath); err != nil { logging.LogWarnf("copy asset from [%s] to [%s] failed: %s", srcAbsPath, targetAbsPath, err) } }
srcs := []string{"stage/build/export", "stage/protyle"} for _, src := range srcs { from := filepath.Join(util.WorkingDir, src) to := filepath.Join(savePath, src) if err := filelock.Copy(from, to); err != nil { logging.LogWarnf("copy stage from [%s] to [%s] failed: %s", from, savePath, err) } }
theme := Conf.Appearance.ThemeLight if 1 == Conf.Appearance.Mode { theme = Conf.Appearance.ThemeDark } // 复制主题文件夹 srcs = []string{"themes/" + theme} 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 appearancePath, readErr = filepath.EvalSymlinks(util.AppearancePath) if nil != readErr { logging.LogErrorf("readlink [%s] failed: %s", util.AppearancePath, readErr) return } }
for _, src := range srcs { from := filepath.Join(appearancePath, src) to := filepath.Join(savePath, "appearance", src) if err := filelock.Copy(from, to); err != nil { logging.LogErrorf("copy appearance from [%s] to [%s] failed: %s", from, savePath, err) return } }
// 只复制图标文件夹中的 icon.js 文件 iconName := Conf.Appearance.Icon // 如果使用的不是内建图标(ant 或 material),需要复制 material 作为后备 if iconName != "ant" && iconName != "material" && iconName != "" { srcIconFile := filepath.Join(appearancePath, "icons", "material", "icon.js") toIconDir := filepath.Join(savePath, "appearance", "icons", "material") if err := os.MkdirAll(toIconDir, 0755); err != nil { logging.LogErrorf("mkdir [%s] failed: %s", toIconDir, err) return } toIconFile := filepath.Join(toIconDir, "icon.js") if err := filelock.Copy(srcIconFile, toIconFile); err != nil { logging.LogWarnf("copy icon file from [%s] to [%s] failed: %s", srcIconFile, toIconFile, err) } } // 复制当前使用的图标文件 if iconName != "" { srcIconFile := filepath.Join(appearancePath, "icons", iconName, "icon.js") toIconDir := filepath.Join(savePath, "appearance", "icons", iconName) if err := os.MkdirAll(toIconDir, 0755); err != nil { logging.LogErrorf("mkdir [%s] failed: %s", toIconDir, err) return } toIconFile := filepath.Join(toIconDir, "icon.js") if err := filelock.Copy(srcIconFile, toIconFile); err != nil { logging.LogWarnf("copy icon file from [%s] to [%s] failed: %s", srcIconFile, toIconFile, err) } }
// 复制自定义表情图片 emojis := emojisInTree(tree) for _, emoji := range emojis { from := filepath.Join(util.DataDir, emoji) to := filepath.Join(savePath, emoji) if err := filelock.Copy(from, to); err != nil { logging.LogErrorf("copy emojis from [%s] to [%s] failed: %s", from, to, err) } }
if docx { processIFrame(tree) fillThemeStyleVar(tree) }
luteEngine := NewLute() luteEngine.SetFootnotes(true)
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue } if ast.NodeEmojiImg == n.Type { // 自定义表情图片地址去掉开头的 / n.Tokens = bytes.ReplaceAll(n.Tokens, []byte("src=\"/emojis"), []byte("src=\"emojis")) } 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 } } } 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.Next.Tokens, []byte(editor.Zwsp)) } } return ast.WalkContinue })
if docx { renderer := render.NewProtyleExportDocxRenderer(tree, luteEngine.RenderOptions, luteEngine.ParseOptions) output := renderer.Render() dom = gulu.Str.FromBytes(output) } else { dom = luteEngine.ProtylePreview(tree, luteEngine.RenderOptions, luteEngine.ParseOptions) } return}
func ExportHTML(id, savePath string, pdf, image, keepFold, merge bool) (name, dom string, node *ast.Node) { savePath = strings.TrimSpace(savePath)
bt := treenode.GetBlockTree(id) if nil == bt { return }
tree := prepareExportTree(bt) node = treenode.GetNodeInTree(tree, id) if ast.NodeDocument == node.Type { node.RemoveIALAttr("style") }
if merge { var mergeErr error tree, mergeErr = mergeSubDocs(tree) if nil != mergeErr { logging.LogErrorf("merge sub docs failed: %s", mergeErr) return } }
blockRefMode := Conf.Export.BlockRefMode 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) && !n.ParentIs(ast.NodeCallout) { headings = append(headings, n) return ast.WalkSkipChildren } return ast.WalkContinue })
for _, h := range headings { link := &ast.Node{Type: ast.NodeLink} link.AppendChild(&ast.Node{Type: ast.NodeOpenBracket}) link.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(" ")}) link.AppendChild(&ast.Node{Type: ast.NodeCloseBracket}) link.AppendChild(&ast.Node{Type: ast.NodeOpenParen}) link.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(PdfOutlineScheme + "://" + h.ID)}) link.AppendChild(&ast.Node{Type: ast.NodeCloseParen}) h.PrependChild(link) } }
tree = exportTree(tree, true, keepFold, true, blockRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode, Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker, Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight, Conf.Export.AddTitle, Conf.Export.InlineMemo, true, true) adjustHeadingLevel(bt, tree) name = path.Base(tree.HPath) name = util.FilterFileName(name) // 导出 PDF、HTML 和 Word 时未移除不支持的文件名符号 https://github.com/siyuan-note/siyuan/issues/5614
if "" != savePath { if err := os.MkdirAll(savePath, 0755); err != nil { logging.LogErrorf("mkdir [%s] failed: %s", savePath, err) return }
assets := getAssetsLinkDests(tree.Root, false) for _, asset := range assets { if strings.Contains(asset, "?") { asset = asset[:strings.LastIndex(asset, "?")] }
srcAbsPath, err := GetAssetAbsPath(asset) if err != nil { logging.LogWarnf("resolve path of asset [%s] failed: %s", asset, err) continue } targetAbsPath := filepath.Join(savePath, asset) if err = filelock.Copy(srcAbsPath, targetAbsPath); err != nil { logging.LogWarnf("copy asset from [%s] to [%s] failed: %s", srcAbsPath, targetAbsPath, err) } } }
if !pdf && "" != savePath { // 导出 HTML 需要复制静态资源 srcs := []string{"stage/build/export", "stage/protyle"} for _, src := range srcs { from := filepath.Join(util.WorkingDir, src) to := filepath.Join(savePath, src) if err := filelock.Copy(from, to); err != nil { logging.LogErrorf("copy stage from [%s] to [%s] failed: %s", from, savePath, err) return } }
theme := Conf.Appearance.ThemeLight if 1 == Conf.Appearance.Mode { theme = Conf.Appearance.ThemeDark } // 复制主题文件夹 srcs = []string{"themes/" + theme} 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 appearancePath, readErr = filepath.EvalSymlinks(util.AppearancePath) if nil != readErr { logging.LogErrorf("readlink [%s] failed: %s", util.AppearancePath, readErr) return } } for _, src := range srcs { from := filepath.Join(appearancePath, src) to := filepath.Join(savePath, "appearance", src) if err := filelock.Copy(from, to); err != nil { logging.LogErrorf("copy appearance from [%s] to [%s] failed: %s", from, savePath, err) } }
// 只复制图标文件夹中的 icon.js 文件 iconName := Conf.Appearance.Icon // 如果使用的不是内建图标(ant 或 material),需要复制 material 作为后备 if iconName != "ant" && iconName != "material" && iconName != "" { srcIconFile := filepath.Join(appearancePath, "icons", "material", "icon.js") toIconDir := filepath.Join(savePath, "appearance", "icons", "material") if err := os.MkdirAll(toIconDir, 0755); err != nil { logging.LogErrorf("mkdir [%s] failed: %s", toIconDir, err) return } toIconFile := filepath.Join(toIconDir, "icon.js") if err := filelock.Copy(srcIconFile, toIconFile); err != nil { logging.LogWarnf("copy icon file from [%s] to [%s] failed: %s", srcIconFile, toIconFile, err) } } // 复制当前使用的图标文件 if iconName != "" { srcIconFile := filepath.Join(appearancePath, "icons", iconName, "icon.js") toIconDir := filepath.Join(savePath, "appearance", "icons", iconName) if err := os.MkdirAll(toIconDir, 0755); err != nil { logging.LogErrorf("mkdir [%s] failed: %s", toIconDir, err) return } toIconFile := filepath.Join(toIconDir, "icon.js") if err := filelock.Copy(srcIconFile, toIconFile); err != nil { logging.LogWarnf("copy icon file from [%s] to [%s] failed: %s", srcIconFile, toIconFile, err) } }
// 复制自定义表情图片 emojis := emojisInTree(tree) for _, emoji := range emojis { from := filepath.Join(util.DataDir, emoji) to := filepath.Join(savePath, emoji) if err := filelock.Copy(from, to); err != nil { logging.LogErrorf("copy emojis from [%s] to [%s] failed: %s", from, to, err) } } }
if pdf { processIFrame(tree) }
luteEngine := NewLute() luteEngine.SetFootnotes(true) luteEngine.RenderOptions.ProtyleContenteditable = false luteEngine.SetProtyleMarkNetImg(false)
// 不进行安全过滤,因为导出时需要保留所有的 HTML 标签 // 使用属性 `data-export-html` 导出时 `<style></style>` 标签丢失 https://github.com/siyuan-note/siyuan/issues/6228 luteEngine.SetSanitize(false)
renderer := render.NewProtyleExportRenderer(tree, luteEngine.RenderOptions, luteEngine.ParseOptions) dom = gulu.Str.FromBytes(renderer.Render()) return}
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) } }
oldRoot := ret.Root ret = parse.Parse("", []byte(""), luteEngine.ParseOptions) first := ret.Root.FirstChild for _, n := range nodes { first.InsertBefore(n) } ret.Root.KramdownIAL = oldRoot.KramdownIAL } ret.Path = bt.Path ret.HPath = bt.HPath ret.Box = bt.BoxID ret.ID = bt.RootID return}
func processIFrame(tree *parse.Tree) { // 导出 PDF/Word 时 IFrame 块使用超链接 https://github.com/siyuan-note/siyuan/issues/4035 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering || ast.NodeIFrame != n.Type { return ast.WalkContinue }
n.Type = ast.NodeParagraph index := bytes.Index(n.Tokens, []byte("src=\"")) if 0 > index { n.AppendChild(&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.AppendChild(link) } return ast.WalkContinue })}
func ProcessPDF(id, p string, merge, removeAssets, watermark bool) (err error) { tree, _ := LoadTreeByBlockID(id) if nil == tree { return }
if merge { var mergeErr error tree, mergeErr = mergeSubDocs(tree) if nil != mergeErr { logging.LogErrorf("merge sub docs failed: %s", mergeErr) return } }
var headings []*ast.Node assetDests := getAssetsLinkDests(tree.Root, false) ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue }
if ast.NodeHeading == n.Type && !n.ParentIs(ast.NodeBlockquote) && !n.ParentIs(ast.NodeCallout) { headings = append(headings, n) return ast.WalkSkipChildren } return ast.WalkContinue })
api.DisableConfigDir() 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 } if loadErr := api.LoadUserFonts(); nil != loadErr { logging.LogErrorf("load user fonts failed: %s", loadErr) }
pdfCtx, ctxErr := api.ReadContextFile(p) if nil != ctxErr { logging.LogErrorf("read pdf context failed: %s", ctxErr) return }
processPDFBookmarks(pdfCtx, headings) processPDFLinkEmbedAssets(pdfCtx, assetDests, removeAssets) processPDFWatermark(pdfCtx, watermark)
pdfcpuVer := model.VersionStr model.VersionStr = "SiYuan v" + util.Ver + " (pdfcpu " + pdfcpuVer + ")" if writeErr := api.WriteContextFile(pdfCtx, p); nil != writeErr { logging.LogErrorf("write pdf context failed: %s", writeErr) return } return}
func processPDFWatermark(pdfCtx *model.Context, watermark bool) { // Support adding the watermark on export PDF https://github.com/siyuan-note/siyuan/issues/9961 // https://pdfcpu.io/core/watermark
if !watermark { return }
str := Conf.Export.PDFWatermarkStr if "" == str { return }
mode := "text" if gulu.File.IsExist(str) { if ".pdf" == strings.ToLower(filepath.Ext(str)) { mode = "pdf" } else { mode = "image" } }
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] }
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) } }
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] }
logging.LogInfof("add PDF watermark [mode=%s, str=%s, desc=%s]", mode, str, desc)
var wm *model.Watermark var err error switch mode { case "text": wm, err = pdfcpu.ParseTextWatermarkDetails(str, desc, false, types.POINTS) case "image": wm, err = pdfcpu.ParseImageWatermarkDetails(str, desc, false, types.POINTS) case "pdf": wm, err = pdfcpu.ParsePDFWatermarkDetails(str, desc, false, types.POINTS) }
if err != nil { logging.LogErrorf("parse watermark failed: %s", err) util.PushErrMsg(err.Error(), 7000) return }
wm.OnTop = true // Export PDF and add watermarks no longer covered by images https://github.com/siyuan-note/siyuan/issues/10818 err = pdfcpu.AddWatermarks(pdfCtx, nil, wm) if err != nil { logging.LogErrorf("add watermark failed: %s", err) return }}
func processPDFBookmarks(pdfCtx *model.Context, headings []*ast.Node) { links, err := PdfListToCLinks(pdfCtx) if err != nil { return }
sort.Slice(links, func(i, j int) bool { return links[i].Page < links[j].Page })
titles := map[string]bool{} bms := map[string]*pdfcpu.Bookmark{} 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) for { if _, ok := titles[title]; ok { title += "\x01" } else { titles[title] = true break } } bm := &pdfcpu.Bookmark{ Title: title, PageFrom: link.Page, AbsPos: link.Rect.UR.Y, } bms[linkID] = bm }
if 1 > len(bms) { return }
var topBms []*pdfcpu.Bookmark stack := linkedliststack.New() for _, h := range headings { L: for ; ; stack.Pop() { cur, ok := stack.Peek() if !ok { bm, ok := bms[h.ID] if !ok { break L } bm.Level = h.HeadingLevel stack.Push(bm) topBms = append(topBms, bm) break L }
tip := cur.(*pdfcpu.Bookmark) if tip.Level < h.HeadingLevel { bm := bms[h.ID] bm.Level = h.HeadingLevel bm.Parent = tip tip.Kids = append(tip.Kids, bm) stack.Push(bm) break L } } }
err = pdfcpu.AddBookmarks(pdfCtx, topBms, true) if err != nil { logging.LogErrorf("add bookmark failed: %s", err) return }}
// processPDFLinkEmbedAssets 处理资源文件超链接,根据 removeAssets 参数决定是否将资源文件嵌入到 PDF 中。// 导出 PDF 时支持将资源文件作为附件嵌入 https://github.com/siyuan-note/siyuan/issues/7414func processPDFLinkEmbedAssets(pdfCtx *model.Context, assetDests []string, removeAssets bool) { var assetAbsPaths []string for _, dest := range assetDests { if absPath, _ := GetAssetAbsPath(dest); "" != absPath { assetAbsPaths = append(assetAbsPaths, absPath) } }
if 1 > len(assetAbsPaths) { return }
assetLinks, otherLinks, listErr := PdfListLinks(pdfCtx) if nil != listErr { logging.LogErrorf("list asset links failed: %s", listErr) return }
if 1 > len(assetLinks) { return }
if _, removeErr := pdfcpu.RemoveAnnotations(pdfCtx, nil, nil, nil, false); nil != removeErr { logging.LogWarnf("remove annotations failed: %s", removeErr) }
linkMap := map[int][]model.AnnotationRenderer{} for _, link := range otherLinks { link.URI, _ = url.PathUnescape(link.URI) if 1 > len(linkMap[link.Page]) { linkMap[link.Page] = []model.AnnotationRenderer{link} } else { linkMap[link.Page] = append(linkMap[link.Page], link) } }
attachmentMap := map[int][]*types.IndirectRef{} now := types.StringLiteral(types.DateString(time.Now())) for _, link := range assetLinks { link.URI = strings.ReplaceAll(link.URI, "http://"+util.LocalHost+":"+util.ServerPort+"/export/temp/", "") link.URI = strings.ReplaceAll(link.URI, "http://"+util.LocalHost+":6806/export/temp/", "") 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 link.URI = strings.ReplaceAll(link.URI, "http://"+util.LocalHost+":6806/", "") link.URI, _ = url.PathUnescape(link.URI) if idx := strings.Index(link.URI, "?"); 0 < idx { link.URI = link.URI[:idx] }
if !removeAssets { // 不移除资源文件夹的话将超链接指向资源文件夹 if 1 > len(linkMap[link.Page]) { linkMap[link.Page] = []model.AnnotationRenderer{link} } else { linkMap[link.Page] = append(linkMap[link.Page], link) }
continue }
// 移除资源文件夹的话使用内嵌附件
absPath, getErr := GetAssetAbsPath(link.URI) if nil != getErr { continue }
ir, newErr := pdfCtx.XRefTable.NewEmbeddedFileStreamDict(absPath) if nil != newErr { logging.LogWarnf("new embedded file stream dict failed: %s", newErr) continue }
fn := filepath.Base(absPath) fileSpecDict, newErr := pdfCtx.XRefTable.NewFileSpecDict(fn, fn, "attached by SiYuan", *ir) if nil != newErr { logging.LogWarnf("new file spec dict failed: %s", newErr) continue }
ir, indErr := pdfCtx.XRefTable.IndRefForNewObject(fileSpecDict) if nil != indErr { logging.LogWarnf("ind ref for new object failed: %s", indErr) continue }
lx := link.Rect.LL.X + link.Rect.Width() ly := link.Rect.LL.Y + link.Rect.Height()/2 w := link.Rect.Height() / 2 h := link.Rect.Height() / 2
d := types.Dict( map[string]types.Object{ "Type": types.Name("Annot"), "Subtype": types.Name("FileAttachment"), "Contents": types.StringLiteral(""), "Rect": types.RectForWidthAndHeight(lx, ly, w, h).Array(), "P": link.P, "M": now, "F": types.Integer(0), "Border": types.NewIntegerArray(0, 0, 1), "C": types.NewNumberArray(0.5, 0.0, 0.5), "CA": types.Float(0.95), "CreationDate": now, "Name": types.Name("FileAttachment"), "FS": *ir, "NM": types.StringLiteral(""), }, )
ann, indErr := pdfCtx.XRefTable.IndRefForNewObject(d) if nil != indErr { logging.LogWarnf("ind ref for new object failed: %s", indErr) continue }
pageDictIndRef, pageErr := pdfCtx.PageDictIndRef(link.Page) if nil != pageErr { logging.LogWarnf("page dict ind ref failed: %s", pageErr) continue }
d, defErr := pdfCtx.DereferenceDict(*pageDictIndRef) if nil != defErr { logging.LogWarnf("dereference dict failed: %s", defErr) continue }
if 1 > len(attachmentMap[link.Page]) { attachmentMap[link.Page] = []*types.IndirectRef{ann} } else { attachmentMap[link.Page] = append(attachmentMap[link.Page], ann) } }
if 0 < len(linkMap) { if _, addErr := pdfcpu.AddAnnotationsMap(pdfCtx, linkMap, false); nil != addErr { logging.LogErrorf("add annotations map failed: %s", addErr) } }
// 添加附件注解指向内嵌的附件 for page, anns := range attachmentMap { pageDictIndRef, pageErr := pdfCtx.PageDictIndRef(page) if nil != pageErr { logging.LogWarnf("page dict ind ref failed: %s", pageErr) continue }
pageDict, defErr := pdfCtx.DereferenceDict(*pageDictIndRef) if nil != defErr { logging.LogWarnf("dereference dict failed: %s", defErr) continue }
array := types.Array{} for _, ann := range anns { array = append(array, *ann) }
obj, found := pageDict.Find("Annots") if !found { pageDict.Insert("Annots", array) pdfCtx.EnsureVersionForWriting() continue }
ir, ok := obj.(types.IndirectRef) if !ok { pageDict.Update("Annots", append(obj.(types.Array), array...)) pdfCtx.EnsureVersionForWriting() continue }
// Annots array is an IndirectReference.
o, err := pdfCtx.Dereference(ir) if err != nil || o == nil { continue }
annots, _ := o.(types.Array) entry, ok := pdfCtx.FindTableEntryForIndRef(&ir) if !ok { continue } entry.Object = append(annots, array...) pdfCtx.EnsureVersionForWriting() }}
func ExportStdMarkdown(id string, assetsDestSpace2Underscore, fillCSSVar, adjustHeadingLevel, imgTag bool) string { bt := treenode.GetBlockTree(id) if nil == bt { logging.LogErrorf("block tree [%s] not found", id) return "" }
tree := prepareExportTree(bt) cloudAssetsBase := "" if IsSubscriber() { cloudAssetsBase = util.GetCloudAssetsServer() + Conf.GetUser().UserId + "/" }
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)
return exportMarkdownContent0(id, tree, cloudAssetsBase, assetsDestSpace2Underscore, adjustHeadingLevel, imgTag, ".md", Conf.Export.BlockRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode, Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker, Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight, Conf.Export.AddTitle, Conf.Export.InlineMemo, defBlockIDs, true, fillCSSVar)}
func ExportPandocConvertZip(ids []string, pandocTo, ext string) (name, zipPath string) { block := treenode.GetBlockTree(ids[0]) box := Conf.Box(block.BoxID) baseFolderName := path.Base(block.HPath) if "." == baseFolderName { baseFolderName = path.Base(block.Path) }
var docPaths []string bts := treenode.GetBlockTrees(ids) for _, bt := range bts { docPaths = append(docPaths, bt.Path)
if Conf.Export.IncludeSubDocs { docFiles := box.ListFiles(strings.TrimSuffix(bt.Path, ".sy")) for _, docFile := range docFiles { docPaths = append(docPaths, docFile.path) } } }
defBlockIDs, docPaths := prepareExportTrees(docPaths) zipPath = exportPandocConvertZip(baseFolderName, docPaths, defBlockIDs, "gfm+footnotes+hard_line_breaks", pandocTo, ext) name = util.GetTreeID(block.Path) return}
func ExportNotebookMarkdown(boxID string) (zipPath string) { util.PushEndlessProgress(Conf.Language(65)) defer util.ClearPushProgress(100)
box := Conf.Box(boxID) if nil == box { logging.LogErrorf("not found box [%s]", boxID) return }
var docPaths []string docFiles := box.ListFiles("/") for _, docFile := range docFiles { docPaths = append(docPaths, docFile.path) }
defBlockIDs, docPaths := prepareExportTrees(docPaths) zipPath = exportPandocConvertZip(box.Name, docPaths, defBlockIDs, "", "", ".md") return}
func yfm(docIAL map[string]string) string { // 导出 Markdown 文件时开头附上一些元数据 https://github.com/siyuan-note/siyuan/issues/6880
buf := bytes.Buffer{} buf.WriteString("---\n") var title, created, updated, tags string for k, v := range docIAL { if "id" == k { createdTime, parseErr := time.ParseInLocation("20060102150405", util.TimeFromID(v), time.Local) if nil == parseErr { created = createdTime.Format(time.RFC3339) } continue } if "title" == k { title = v continue } if "updated" == k { updatedTime, parseErr := time.ParseInLocation("20060102150405", v, time.Local) 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 } buf.WriteString("date: ") buf.WriteString(created) buf.WriteString("\n") buf.WriteString("lastmod: ") buf.WriteString(updated) buf.WriteString("\n") if "" != tags { buf.WriteString("tags: [") buf.WriteString(tags) buf.WriteString("]\n") } buf.WriteString("---\n\n") return buf.String()}
func exportBoxSYZip(boxID string) (zipPath string) { util.PushEndlessProgress(Conf.Language(65)) defer util.ClearPushProgress(100)
box := Conf.Box(boxID) if nil == box { logging.LogErrorf("not found box [%s]", boxID) return } baseFolderName := box.Name
var docPaths []string docFiles := box.ListFiles("/") for _, docFile := range docFiles { docPaths = append(docPaths, docFile.path) } zipPath = exportSYZip(boxID, "/", baseFolderName, docPaths) return}
func exportSYZip(boxID, rootDirPath, baseFolderName string, docPaths []string) (zipPath string) { defer util.ClearPushProgress(100)
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)
exportDir := filepath.Join(util.TempDir, "export", baseFolderName) if err := os.MkdirAll(exportDir, 0755); err != nil { logging.LogErrorf("create export temp folder failed: %s", err) return }
trees := map[string]*parse.Tree{} refTrees := map[string]*parse.Tree{} luteEngine := util.NewLute() for i, p := range docPaths { if !strings.HasSuffix(p, ".sy") { continue }
tree, err := filesys.LoadTree(boxID, p, luteEngine) if err != nil { continue } trees[tree.ID] = tree
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 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"))))
refs := map[string]*parse.Tree{} exportRefTrees(tree, &[]string{}, refs) for refTreeID, refTree := range refs { if nil == trees[refTreeID] { refTrees[refTreeID] = refTree } } count++ }
util.PushEndlessProgress(Conf.Language(65)) count = 0
// 按文件夹结构复制选择的树 total := len(trees) + len(refTrees) for _, tree := range trees { readPath := filepath.Join(util.DataDir, tree.Box, tree.Path) data, readErr := filelock.ReadFile(readPath) if nil != readErr { logging.LogErrorf("read file [%s] failed: %s", readPath, readErr) continue }
writePath := strings.TrimPrefix(tree.Path, rootDirPath) writePath = filepath.Join(exportDir, writePath) writeFolder := filepath.Dir(writePath) if mkdirErr := os.MkdirAll(writeFolder, 0755); nil != mkdirErr { logging.LogErrorf("create export temp folder [%s] failed: %s", writeFolder, mkdirErr) continue } if writeErr := os.WriteFile(writePath, data, 0644); nil != writeErr { logging.LogErrorf("write export file [%s] failed: %s", writePath, writeErr) continue } count++
util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.Language(66), fmt.Sprintf("%d/%d ", count, total)+tree.HPath)) }
count = 0 // 引用树放在导出文件夹根路径下 for treeID, tree := range refTrees { readPath := filepath.Join(util.DataDir, tree.Box, tree.Path) data, readErr := filelock.ReadFile(readPath) if nil != readErr { logging.LogErrorf("read file [%s] failed: %s", readPath, readErr) continue }
writePath := strings.TrimPrefix(tree.Path, rootDirPath) writePath = filepath.Join(exportDir, treeID+".sy") if writeErr := os.WriteFile(writePath, data, 0644); nil != writeErr { logging.LogErrorf("write export file [%s] failed: %s", writePath, writeErr) continue } count++
util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.Language(66), fmt.Sprintf("%d/%d ", count, total)+tree.HPath)) }
// 将引用树合并到选择树中,以便后面一次性导出资源文件 for treeID, tree := range refTrees { trees[treeID] = tree }
// 导出引用的资源文件 assetPathMap, err := allAssetAbsPaths() if nil != err { logging.LogWarnf("get assets abs path failed: %s", err) return } copiedAssets := hashset.New() for _, tree := range trees { var assets []string assets = append(assets, getAssetsLinkDests(tree.Root, false)...) titleImgPath := treenode.GetDocTitleImgPath(tree.Root) // Export .sy.zip doc title image is not exported https://github.com/siyuan-note/siyuan/issues/8748 if "" != titleImgPath { if util.IsAssetLinkDest([]byte(titleImgPath), false) { assets = append(assets, titleImgPath) } }
for _, asset := range assets { util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(70), asset))
asset = string(html.DecodeDestination([]byte(asset))) if strings.Contains(asset, "?") { asset = asset[:strings.LastIndex(asset, "?")] }
if copiedAssets.Contains(asset) { continue }
srcPath := assetPathMap[asset] if "" == srcPath { logging.LogWarnf("get asset [%s] abs path failed", asset) continue }
destPath := filepath.Join(exportDir, asset) assetErr := filelock.Copy(srcPath, destPath) if nil != assetErr { logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", srcPath, destPath, assetErr) continue }
if !gulu.File.IsDir(srcPath) && strings.HasSuffix(strings.ToLower(srcPath), ".pdf") { sya := srcPath + ".sya" if filelock.IsExist(sya) { // 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) } } }
copiedAssets.Add(asset) }
// 复制自定义表情图片 emojis := emojisInTree(tree) for _, emoji := range emojis { from := filepath.Join(util.DataDir, emoji) to := filepath.Join(exportDir, emoji) if copyErr := filelock.Copy(from, to); copyErr != nil { logging.LogErrorf("copy emojis from [%s] to [%s] failed: %s", from, to, copyErr) } } }
// 导出数据库 Attribute View export https://github.com/siyuan-note/siyuan/issues/8710 exportStorageAvDir := filepath.Join(exportDir, "storage", "av") var avIDs []string for _, tree := range trees { ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering || !n.IsBlock() { return ast.WalkContinue }
if ast.NodeAttributeView == n.Type { avIDs = append(avIDs, n.AttributeViewID) }
avs := n.IALAttr(av.NodeAttrNameAvs) if "" == avs { return ast.WalkContinue }
for _, avID := range strings.Split(avs, ",") { avIDs = append(avIDs, strings.TrimSpace(avID)) } return ast.WalkContinue }) } avIDs = gulu.Str.RemoveDuplicatedElem(avIDs) for _, avID := range avIDs { if !ast.IsNodeIDPattern(avID) { continue }
exportAv(avID, exportStorageAvDir, exportDir, assetPathMap) }
// 导出闪卡 Export related flashcard data when exporting .sy.zip https://github.com/siyuan-note/siyuan/issues/9372 exportStorageRiffDir := filepath.Join(exportDir, "storage", "riff") 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)
for _, card := range cards { deck.AddCard(card.ID(), card.BlockID()) } } if 0 < deck.CountCards() { if saveErr := deck.Save(); nil != saveErr { logging.LogErrorf("save deck [%s] failed: %s", name, saveErr) } } }
// 导出自定义排序 sortPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json") fullSortIDs := map[string]int{} sortIDs := map[string]int{} var sortData []byte var sortErr error if filelock.IsExist(sortPath) { sortData, sortErr = filelock.ReadFile(sortPath) if nil != sortErr { logging.LogErrorf("read sort conf failed: %s", sortErr) }
if sortErr = gulu.JSON.UnmarshalJSON(sortData, &fullSortIDs); nil != sortErr { logging.LogErrorf("unmarshal sort conf failed: %s", sortErr) }
if 0 < len(fullSortIDs) { for _, tree := range trees { if v, ok := fullSortIDs[tree.ID]; ok { sortIDs[tree.ID] = v } } }
if 0 < len(sortIDs) { sortData, sortErr = gulu.JSON.MarshalJSON(sortIDs) if nil != sortErr { logging.LogErrorf("marshal sort conf failed: %s", sortErr) } if 0 < len(sortData) { confDir := filepath.Join(exportDir, ".siyuan") if mkdirErr := os.MkdirAll(confDir, 0755); nil != mkdirErr { logging.LogErrorf("create export conf folder [%s] failed: %s", confDir, mkdirErr) } else { sortPath = filepath.Join(confDir, "sort.json") if writeErr := os.WriteFile(sortPath, sortData, 0644); nil != writeErr { logging.LogErrorf("write sort conf failed: %s", writeErr) } } } } }
zipPath = exportDir + ".sy.zip" zip, err := gulu.Zip.Create(zipPath) if err != nil { logging.LogErrorf("create export .sy.zip [%s] failed: %s", exportDir, err) return "" }
zipCallback := func(filename string) { util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(253), filename)) }
if err = zip.AddDirectory(baseFolderName, exportDir, zipCallback); err != nil { logging.LogErrorf("create export .sy.zip [%s] failed: %s", exportDir, err) return "" }
if err = zip.Close(); err != nil { logging.LogErrorf("close export .sy.zip failed: %s", err) }
os.RemoveAll(exportDir) zipPath = "/export/" + url.PathEscape(filepath.Base(zipPath)) return}
func exportAv(avID, exportStorageAvDir, exportFolder string, assetPathMap map[string]string) { avJSONPath := av.GetAttributeViewDataPath(avID) if !filelock.IsExist(avJSONPath) { return }
if copyErr := filelock.Copy(avJSONPath, filepath.Join(exportStorageAvDir, avID+".json")); nil != copyErr { logging.LogErrorf("copy av json failed: %s", copyErr) }
attrView, err := av.ParseAttributeView(avID) if err != nil { logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err) return }
for _, keyValues := range attrView.KeyValues { switch keyValues.Key.Type { case av.KeyTypeMAsset: // 导出资源文件列 https://github.com/siyuan-note/siyuan/issues/9919 for _, value := range keyValues.Values { for _, asset := range value.MAsset { if !util.IsAssetLinkDest([]byte(asset.Content), false) { continue }
destPath := filepath.Join(exportFolder, asset.Content) srcPath := assetPathMap[asset.Content] if "" == srcPath { logging.LogWarnf("get asset [%s] abs path failed", asset.Content) continue }
if copyErr := filelock.Copy(srcPath, destPath); nil != copyErr { logging.LogErrorf("copy asset failed: %s", copyErr) } } } } }
// 级联导出关联列关联的数据库 exportRelationAvs(avID, exportStorageAvDir)}
func exportRelationAvs(avID, exportStorageAvDir string) { avIDs := hashset.New() walkRelationAvs(avID, avIDs)
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) } }}
func walkRelationAvs(avID string, exportAvIDs *hashset.Set) { 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 }
walkRelationAvs(keyValues.Key.Relation.AvID, exportAvIDs) } }}
func ExportMarkdownContent(id string, refMode, embedMode int, addYfm, fillCSSVar, adjustHeadingLv, imgTag, addTitle bool) (hPath, exportedMd string) { bt := treenode.GetBlockTree(id) if nil == bt { return }
tree := prepareExportTree(bt) hPath = tree.HPath exportedMd = exportMarkdownContent0(id, tree, "", false, adjustHeadingLv, imgTag, ".md", refMode, embedMode, Conf.Export.FileAnnotationRefMode, Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker, Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight, addTitle, Conf.Export.InlineMemo, nil, true, fillCSSVar) docIAL := parse.IAL2Map(tree.Root.KramdownIAL) if addYfm { exportedMd = yfm(docIAL) + exportedMd } return}
func exportMarkdownContent(rootID, ext string, exportRefMode int, defBlockIDs []string, singleFile bool) (tree *parse.Tree, exportedMd string, isEmpty bool) { tree, err := LoadTreeByBlockID(rootID) if err != nil { logging.LogErrorf("load tree by block id [%s] failed: %s", rootID, err) return }
refCount := sql.QueryRootChildrenRefCount(tree.ID) if !Conf.Export.MarkdownYFM && treenode.ContainOnlyDefaultIAL(tree) && 1 > len(refCount) { for c := tree.Root.FirstChild; nil != c; c = c.Next { if ast.NodeParagraph == c.Type { isEmpty = nil == c.FirstChild if !isEmpty { break } } else { isEmpty = false break } } }
exportedMd = exportMarkdownContent0(rootID, tree, "", false, false, false, ext, exportRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode, Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker, Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight, Conf.Export.AddTitle, Conf.Export.InlineMemo, defBlockIDs, singleFile, false) docIAL := parse.IAL2Map(tree.Root.KramdownIAL) if Conf.Export.MarkdownYFM { // 导出 Markdown 时在文档头添加 YFM 开关 https://github.com/siyuan-note/siyuan/issues/7727 exportedMd = yfm(docIAL) + exportedMd } return}
func exportMarkdownContent0(id string, tree *parse.Tree, cloudAssetsBase string, assetsDestSpace2Underscore, adjustHeadingLv, imgTag bool, ext string, blockRefMode, blockEmbedMode, fileAnnotationRefMode int, tagOpenMarker, tagCloseMarker string, blockRefTextLeft, blockRefTextRight string, addTitle, inlineMemo bool, defBlockIDs []string, singleFile, fillCSSVar bool) (ret string) { tree = exportTree(tree, false, false, false, blockRefMode, blockEmbedMode, fileAnnotationRefMode, tagOpenMarker, tagCloseMarker, blockRefTextLeft, blockRefTextRight, addTitle, inlineMemo, 0 < len(defBlockIDs), singleFile) if adjustHeadingLv { bt := treenode.GetBlockTree(id) adjustHeadingLevel(bt, tree) }
luteEngine := NewLute() luteEngine.SetFootnotes(true) luteEngine.SetKramdownIAL(false) if "" != cloudAssetsBase { luteEngine.RenderOptions.LinkBase = cloudAssetsBase } 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, false) { n.Tokens = bytes.ReplaceAll(n.Tokens, []byte(" "), []byte("_")) } } else if n.IsTextMarkType("a") { href := n.TextMarkAHref if util.IsAssetLinkDest([]byte(href), false) { n.TextMarkAHref = strings.ReplaceAll(href, " ", "_") } } else if ast.NodeIFrame == n.Type || ast.NodeAudio == n.Type || ast.NodeVideo == n.Type { dest := treenode.GetNodeSrcTokens(n) if util.IsAssetLinkDest([]byte(dest), false) { setAssetsLinkDest(n, dest, strings.ReplaceAll(dest, " ", "_")) } } return ast.WalkContinue }) }
currentDocDir := path.Dir(tree.HPath) currentDocDir = util.FilterFilePath(currentDocDir)
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) { // When exporting Markdown, `<br />` nodes in non-tables are replaced with `\n` text nodes https://github.com/siyuan-note/siyuan/issues/9509 n.InsertBefore(&ast.Node{Type: ast.NodeText, Tokens: []byte("\n")}) unlinks = append(unlinks, n) } }
if 4 == blockRefMode { // 脚注+锚点哈希 if n.IsBlock() && gulu.Str.Contains(n.ID, defBlockIDs) { // 如果是定义块,则在开头处添加锚点 anchorSpan := treenode.NewSpanAnchor(n.ID) if ast.NodeDocument != n.Type { firstLeaf := treenode.FirstLeafBlock(n) 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) } } } 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 { href += bt.HPath + ext if "d" != bt.Type { href += "#" + defID } if tree.ID == bt.RootID { href = "#" + defID } }
sameDir := path.Dir(href) == currentDocDir if strings.HasPrefix(href, "#") { sameDir = true } href = util.FilterFilePath(href) if !sameDir { var relErr error href, relErr = filepath.Rel(currentDocDir, href) if nil != relErr { logging.LogWarnf("get relative path from [%s] to [%s] failed: %s", currentDocDir, href, relErr) } href = filepath.ToSlash(href) } else { href = strings.TrimPrefix(href, currentDocDir+"/") } blockRefLink := &ast.Node{Type: ast.NodeTextMark, TextMarkType: "a", TextMarkTextContent: linkText, TextMarkAHref: href} blockRefLink.KramdownIAL = n.KramdownIAL n.InsertBefore(blockRefLink) unlinks = append(unlinks, n) } } } return ast.WalkContinue }) for _, unlink := range unlinks { unlink.Unlink() }
if fillCSSVar { fillThemeStyleVar(tree) }
luteEngine.SetUnorderedListMarker("-") luteEngine.SetImgTag(imgTag) renderer := render.NewProtyleExportMdRenderer(tree, luteEngine.RenderOptions, luteEngine.ParseOptions) ret = gulu.Str.FromBytes(renderer.Render()) return}
func exportTree(tree *parse.Tree, wysiwyg, keepFold, avHiddenCol bool, blockRefMode, blockEmbedMode, fileAnnotationRefMode int, tagOpenMarker, tagCloseMarker string, blockRefTextLeft, blockRefTextRight string, addTitle, inlineMemo, addDocAnchorSpan, singleFile bool) (ret *parse.Tree) { luteEngine := NewLute() ret = tree id := tree.Root.ID
// 解析查询嵌入节点 depth := 0 resolveEmbedR(ret.Root, blockEmbedMode, luteEngine, &[]string{}, &depth)
// 将块超链接转换为引用 depth = 0 blockLink2Ref(ret, ret.ID, &depth)
// 收集引用转脚注+锚点哈希 var refFootnotes []*refAsFootnotes if 4 == blockRefMode && singleFile { depth = 0 collectFootnotesDefs(ret, ret.ID, &refFootnotes, &depth) }
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 })
var unlinks []*ast.Node 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: n.SetIALAttr("id", n.ID) case ast.NodeMathBlockContent: n.Tokens = bytes.TrimSpace(n.Tokens) // 导出 Markdown 时去除公式内容中的首尾空格 https://github.com/siyuan-note/siyuan/issues/4666 return ast.WalkContinue case ast.NodeTextMark: if n.IsTextMarkType("inline-memo") { if !inlineMemo { n.TextMarkInlineMemoContent = "" } }
if n.IsTextMarkType("inline-math") { n.TextMarkInlineMathContent = strings.TrimSpace(n.TextMarkInlineMathContent) return ast.WalkContinue } else if treenode.IsFileAnnotationRef(n) { refID := n.TextMarkFileAnnotationRefID if !strings.Contains(refID, "/") { return ast.WalkSkipChildren }
status := processFileAnnotationRef(refID, n, fileAnnotationRefMode) unlinks = append(unlinks, n) return status } else if n.IsTextMarkType("tag") { if !wysiwyg { n.Type = ast.NodeText n.Tokens = []byte(tagOpenMarker + n.TextMarkTextContent + tagCloseMarker) return ast.WalkContinue } } }
if !treenode.IsBlockRef(n) { return ast.WalkContinue }
// 处理引用节点 defID, linkText := getExportBlockRefLinkText(n, blockRefTextLeft, blockRefTextRight)
switch blockRefMode { case 2: // 锚文本块链 blockRefLink := &ast.Node{Type: ast.NodeTextMark, TextMarkTextContent: linkText, TextMarkAHref: "siyuan://blocks/" + defID} blockRefLink.KramdownIAL = n.KramdownIAL blockRefLink.TextMarkType = "a " + n.TextMarkType blockRefLink.TextMarkInlineMemoContent = n.TextMarkInlineMemoContent n.InsertBefore(blockRefLink) unlinks = append(unlinks, n) case 3: // 仅锚文本 blockRefLink := &ast.Node{Type: ast.NodeTextMark, TextMarkType: strings.TrimSpace(strings.ReplaceAll(n.TextMarkType, "block-ref", "")), TextMarkTextContent: linkText} blockRefLink.KramdownIAL = n.KramdownIAL blockRefLink.TextMarkInlineMemoContent = n.TextMarkInlineMemoContent n.InsertBefore(blockRefLink) unlinks = append(unlinks, n) case 4: // 脚注+锚点哈希 if currentTreeNodeIDs[defID] { // 当前文档内不转换脚注,直接使用锚点哈希 https://github.com/siyuan-note/siyuan/issues/13283 n.TextMarkType = "a " + n.TextMarkType n.TextMarkTextContent = linkText n.TextMarkAHref = "#" + defID return ast.WalkContinue }
refFoot := getRefAsFootnotes(defID, &refFootnotes) if nil == refFoot { return ast.WalkContinue }
text := &ast.Node{Type: ast.NodeText, Tokens: []byte(linkText)} if "block-ref" != n.TextMarkType { text.Type = ast.NodeTextMark text.TextMarkType = strings.TrimSpace(strings.ReplaceAll(n.TextMarkType, "block-ref", "")) text.TextMarkTextContent = linkText text.TextMarkInlineMemoContent = n.TextMarkInlineMemoContent } n.InsertBefore(text) n.InsertBefore(&ast.Node{Type: ast.NodeFootnotesRef, Tokens: []byte("^" + refFoot.refNum), FootnotesRefId: refFoot.refNum, FootnotesRefLabel: []byte("^" + refFoot.refNum)}) unlinks = append(unlinks, n) } return ast.WalkSkipChildren }) for _, n := range unlinks { n.Unlink() }
if 4 == blockRefMode { // 脚注+锚点哈希 unlinks = nil footnotesDefBlock := resolveFootnotesDefs(&refFootnotes, ret, currentTreeNodeIDs, blockRefTextLeft, blockRefTextRight) if nil != footnotesDefBlock { // 如果是聚焦导出,可能存在没有使用的脚注定义块,在这里进行清理 // Improve focus export conversion of block refs to footnotes https://github.com/siyuan-note/siyuan/issues/10647 footnotesRefs := ret.Root.ChildrenByType(ast.NodeFootnotesRef) for footnotesDef := footnotesDefBlock.FirstChild; nil != footnotesDef; footnotesDef = footnotesDef.Next { fnRefsInDef := footnotesDef.ChildrenByType(ast.NodeFootnotesRef) footnotesRefs = append(footnotesRefs, fnRefsInDef...) }
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() }
ret.Root.AppendChild(footnotesDefBlock) } }
if addTitle { if root, _ := getBlock(id, tree); nil != root { root.IAL["type"] = "doc" title := &ast.Node{Type: ast.NodeHeading, HeadingLevel: 1} for k, v := range root.IAL { if "type" == k || "style" == k { continue } title.SetIALAttr(k, v) } title.InsertAfter(&ast.Node{Type: ast.NodeKramdownBlockIAL, Tokens: parse.IAL2Tokens(title.KramdownIAL)}) content := html.UnescapeString(root.Content) title.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(content)}) ret.Root.PrependChild(title) } } else { if 4 == blockRefMode { // 脚注+锚点哈希 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 { anchorSpan := treenode.NewSpanAnchor(id) ret.Root.PrependChild(anchorSpan) } } }
// 导出时支持导出题头图 https://github.com/siyuan-note/siyuan/issues/4372 titleImgPath := treenode.GetDocTitleImgPath(ret.Root) if "" != titleImgPath { p := &ast.Node{Type: ast.NodeParagraph} titleImg := &ast.Node{Type: ast.NodeImage} titleImg.AppendChild(&ast.Node{Type: ast.NodeBang}) titleImg.AppendChild(&ast.Node{Type: ast.NodeOpenBracket}) titleImg.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte("image")}) titleImg.AppendChild(&ast.Node{Type: ast.NodeCloseBracket}) titleImg.AppendChild(&ast.Node{Type: ast.NodeOpenParen}) titleImg.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(titleImgPath)}) titleImg.AppendChild(&ast.Node{Type: ast.NodeCloseParen}) p.AppendChild(titleImg) ret.Root.PrependChild(p) }
unlinks = nil var emptyParagraphs []*ast.Node ast.Walk(ret.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue }
// 支持按照现有折叠状态导出 PDF https://github.com/siyuan-note/siyuan/issues/5941 if !keepFold { // 块折叠以后导出 HTML/PDF 固定展开 https://github.com/siyuan-note/siyuan/issues/4064 n.RemoveIALAttr("fold") n.RemoveIALAttr("heading-fold") } else { if "1" == n.IALAttr("heading-fold") { unlinks = append(unlinks, n) return ast.WalkContinue } }
// 导出时去掉内容块闪卡样式 https://github.com/siyuan-note/siyuan/issues/7374 if n.IsBlock() { n.RemoveIALAttr(NodeAttrRiffDecks) }
switch n.Type { case ast.NodeParagraph: if nil == n.FirstChild { // 空的段落块需要补全文本展位,否则后续格式化后再解析树会语义不一致 https://github.com/siyuan-note/siyuan/issues/5806 emptyParagraphs = append(emptyParagraphs, n) } case ast.NodeWidget: // 挂件块导出 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 } }
exportMdVal := n.IALAttr("data-export-md") exportMdVal = html.UnescapeString(exportMdVal) // 导出 `data-export-md` 时未解析代码块与行内代码内的转义字符 https://github.com/siyuan-note/siyuan/issues/4180 if "" != exportMdVal { luteEngine0 := util.NewLute() luteEngine0.SetYamlFrontMatter(true) // 挂件导出属性 `data-export-md` 支持 YFM https://github.com/siyuan-note/siyuan/issues/7752 exportMdTree := parse.Parse("", []byte(exportMdVal), luteEngine0.ParseOptions) var insertNodes []*ast.Node for c := exportMdTree.Root.FirstChild; nil != c; c = c.Next { if ast.NodeKramdownBlockIAL != c.Type { insertNodes = append(insertNodes, c) } } for _, insertNode := range insertNodes { n.InsertBefore(insertNode) } unlinks = append(unlinks, n) } case ast.NodeSuperBlockOpenMarker, ast.NodeSuperBlockLayoutMarker, ast.NodeSuperBlockCloseMarker: if !wysiwyg { unlinks = append(unlinks, n) } }
if ast.NodeText != n.Type { return ast.WalkContinue }
// Shift+Enter 换行在导出为 Markdown 时使用硬换行 https://github.com/siyuan-note/siyuan/issues/3458 n.Tokens = bytes.ReplaceAll(n.Tokens, []byte("\n"), []byte(" \n")) return ast.WalkContinue }) for _, n := range unlinks { n.Unlink() } for _, emptyParagraph := range emptyParagraphs { emptyParagraph.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(editor.Zwj)}) }
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 if avJSONPath := av.GetAttributeViewDataPath(avID); !filelock.IsExist(avJSONPath) { return ast.WalkContinue }
attrView, err := av.ParseAttributeView(avID) if err != nil { logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err) return ast.WalkContinue }
viewID := n.IALAttr(av.NodeAttrView) view, err := attrView.GetCurrentView(viewID) if err != nil { logging.LogErrorf("get attribute view [%s] failed: %s", avID, err) return ast.WalkContinue }
table := getAttrViewTable(attrView, view, "")
// 遵循视图过滤和排序规则 Use filtering and sorting of current view settings when exporting database blocks https://github.com/siyuan-note/siyuan/issues/10474 cachedAttrViews := map[string]*av.AttributeView{} rollupFurtherCollections := sql.GetFurtherCollections(attrView, cachedAttrViews) av.Filter(table, attrView, rollupFurtherCollections, cachedAttrViews) av.Sort(table, attrView)
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 { if avHiddenCol && col.Hidden { // 按需跳过隐藏列 Improve database table view exporting https://github.com/siyuan-note/siyuan/issues/12232 continue }
cell := &ast.Node{Type: ast.NodeTableCell} name := col.Name if !wysiwyg { name = string(lex.EscapeProtyleMarkers([]byte(col.Name))) name = strings.ReplaceAll(name, "\\|", "|") name = strings.ReplaceAll(name, "|", "\\|") } cell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(name)}) mdTableHeadRow.AppendChild(cell) }
rowNum := 1 for _, row := range table.Rows { mdTableRow := &ast.Node{Type: ast.NodeTableRow, TableAligns: aligns} mdTable.AppendChild(mdTableRow) for _, cell := range row.Cells { if avHiddenCol && nil != cell.Value { if col := table.GetColumn(cell.Value.KeyID); nil != col && col.Hidden { continue } }
mdTableCell := &ast.Node{Type: ast.NodeTableCell} mdTableRow.AppendChild(mdTableCell) var val string if nil != cell.Value { if av.KeyTypeBlock == cell.Value.Type { if nil != cell.Value.Block { val = cell.Value.Block.Content if !wysiwyg { val = string(lex.EscapeProtyleMarkers([]byte(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)}) } continue } } else if av.KeyTypeText == cell.Value.Type { if nil != cell.Value.Text { val = cell.Value.Text.Content if !wysiwyg { val = string(lex.EscapeProtyleMarkers([]byte(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)}) } continue } } else if av.KeyTypeTemplate == cell.Value.Type { if nil != cell.Value.Template { val = cell.Value.Template.Content 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)}) } continue } } else if av.KeyTypeDate == cell.Value.Type { if nil != cell.Value.Date { cell.Value.Date = av.NewFormattedValueDate(cell.Value.Date.Content, cell.Value.Date.Content2, av.DateFormatNone, cell.Value.Date.IsNotTime, cell.Value.Date.HasEndDate) } } else if av.KeyTypeCreated == cell.Value.Type { if nil != cell.Value.Created { key, _ := attrView.GetKey(cell.Value.KeyID) isNotTime := false if nil != key && nil != key.Created { isNotTime = !key.Created.IncludeTime }
cell.Value.Created = av.NewFormattedValueCreated(cell.Value.Created.Content, 0, av.CreatedFormatNone, isNotTime) } } else if av.KeyTypeUpdated == cell.Value.Type { if nil != cell.Value.Updated { key, _ := attrView.GetKey(cell.Value.KeyID) isNotTime := false if nil != key && nil != key.Updated { isNotTime = !key.Updated.IncludeTime }
cell.Value.Updated = av.NewFormattedValueUpdated(cell.Value.Updated.Content, 0, av.UpdatedFormatNone, isNotTime) } } 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 } } else if av.KeyTypeMAsset == cell.Value.Type { if nil != cell.Value.MAsset { for i, a := range cell.Value.MAsset { if av.AssetTypeImage == a.Type { 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) img.SetIALAttr("style", "max-height: 128px;")
width, height := GetAssetImgSize(a.Content) if height > 128 { img.SetIALAttr("height", "128px") newWidth := int(float64(width) * (128.0 / float64(height))) img.SetIALAttr("width", strconv.Itoa(newWidth)+"px") } } else if av.AssetTypeFile == a.Type { linkText := strings.TrimSpace(a.Name) if "" == linkText { linkText = a.Content }
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)}) } } if i < len(cell.Value.MAsset)-1 { mdTableCell.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(" ")}) } } continue } } else if av.KeyTypeLineNumber == cell.Value.Type { val = strconv.Itoa(rowNum) rowNum++ } 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, "|", "\\|") }
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)}) } } 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, "|", "\\|") }
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)}) } } } 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, "|", "\\|") }
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)}) } } 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 }
if "" == val { val = cell.Value.String(true) } } 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() } return ret}
func resolveFootnotesDefs(refFootnotes *[]*refAsFootnotes, currentTree *parse.Tree, currentTreeNodeIDs map[string]bool, blockRefTextLeft, blockRefTextRight string) (footnotesDefBlock *ast.Node) { if 1 > len(*refFootnotes) { return nil }
footnotesDefBlock = &ast.Node{Type: ast.NodeFootnotesDefBlock} var rendered []string
var defIDs []string for _, foot := range *refFootnotes { defIDs = append(defIDs, foot.defID) } defIDs = gulu.Str.RemoveDuplicatedElem(defIDs) bts := treenode.GetBlockTrees(defIDs) for _, foot := range *refFootnotes { bt := bts[foot.defID] if nil == bt { logging.LogWarnf("not found block tree for footnote def [%s] refNum [%s]", foot.defID, foot.refNum) continue }
t, err := LoadTreeByBlockID(bt.RootID) if nil != err { logging.LogWarnf("load tree for footnote def [%s] refNum [%s] failed: %s", foot.defID, foot.refNum, err) continue }
defNode := treenode.GetNodeInTree(t, foot.defID) if nil == defNode { logging.LogErrorf("not found node [%s] in tree for footnote refNum [%s]", foot.defID, foot.refNum) continue }
docID := util.GetTreeID(defNode.Path) var nodes []*ast.Node if ast.NodeHeading == defNode.Type { nodes = append(nodes, defNode) if currentTree.ID != docID { // 同文档块引转脚注缩略定义考虑容器块和标题块 https://github.com/siyuan-note/siyuan/issues/5917 children := treenode.HeadingChildren(defNode) nodes = append(nodes, children...) } } else if ast.NodeDocument == defNode.Type { docTitle := &ast.Node{ID: defNode.ID, Type: ast.NodeHeading, HeadingLevel: 1} docTitle.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(defNode.IALAttr("title"))}) nodes = append(nodes, docTitle) for c := defNode.FirstChild; nil != c; c = c.Next { nodes = append(nodes, c) } } else { nodes = append(nodes, defNode) }
var newNodes []*ast.Node for _, node := range nodes { var unlinks []*ast.Node
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue }
if treenode.IsBlockRef(n) { defID, _, _ := treenode.GetBlockRef(n) if f := getRefAsFootnotes(defID, refFootnotes); nil != f { n.InsertBefore(&ast.Node{Type: ast.NodeText, Tokens: []byte(blockRefTextLeft + f.refAnchorText + blockRefTextRight)}) n.InsertBefore(&ast.Node{Type: ast.NodeFootnotesRef, Tokens: []byte("^" + f.refNum), FootnotesRefId: f.refNum, FootnotesRefLabel: []byte("^" + f.refNum)}) unlinks = append(unlinks, n) } 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 } } } return ast.WalkSkipChildren } else if ast.NodeBlockQueryEmbed == n.Type { stmt := n.ChildByType(ast.NodeBlockQueryEmbedScript).TokensStr() stmt = html.UnescapeString(stmt) stmt = strings.ReplaceAll(stmt, editor.IALValEscNewLine, "\n") sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, Conf.Search.Limit) for _, b := range sqlBlocks { subNodes := renderBlockMarkdownR(b.ID, &rendered) for _, subNode := range subNodes { if ast.NodeListItem == subNode.Type { parentList := &ast.Node{Type: ast.NodeList, ListData: &ast.ListData{Typ: subNode.ListData.Typ}} parentList.AppendChild(subNode) newNodes = append(newNodes, parentList) } else { newNodes = append(newNodes, subNode) } } } unlinks = append(unlinks, n) return ast.WalkSkipChildren } return ast.WalkContinue }) for _, n := range unlinks { n.Unlink() }
if ast.NodeBlockQueryEmbed != node.Type { if ast.NodeListItem == node.Type { parentList := &ast.Node{Type: ast.NodeList, ListData: &ast.ListData{Typ: node.ListData.Typ}} parentList.AppendChild(node) newNodes = append(newNodes, parentList) } else { newNodes = append(newNodes, node) } } }
footnotesDef := &ast.Node{Type: ast.NodeFootnotesDef, Tokens: []byte("^" + foot.refNum), FootnotesRefId: foot.refNum, FootnotesRefLabel: []byte("^" + foot.refNum)} for _, node := range newNodes { ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue } if ast.NodeParagraph != n.Type { return ast.WalkContinue }
docID := util.GetTreeID(n.Path) if currentTree.ID == docID { // 同文档块引转脚注缩略定义 https://github.com/siyuan-note/siyuan/issues/3299 if text := sql.GetRefText(n.ID); 64 < utf8.RuneCountInString(text) { var unlinkChildren []*ast.Node for c := n.FirstChild; nil != c; c = c.Next { unlinkChildren = append(unlinkChildren, c) } for _, c := range unlinkChildren { c.Unlink() } text = gulu.Str.SubStr(text, 64) + "..." n.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(text)}) return ast.WalkSkipChildren } } return ast.WalkContinue })
footnotesDef.AppendChild(node) } footnotesDefBlock.AppendChild(footnotesDef) } return}
func blockLink2Ref(currentTree *parse.Tree, id string, depth *int) { *depth++ if 4096 < *depth { return }
b := treenode.GetBlockTree(id) if nil == b { return } t, err := LoadTreeByBlockID(b.RootID) if nil != err { return }
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, depth) if ast.NodeHeading == node.Type { children := treenode.HeadingChildren(node) for _, c := range children { blockLink2Ref0(currentTree, c, depth) } } return}
func blockLink2Ref0(currentTree *parse.Tree, node *ast.Node, depth *int) { ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue }
if treenode.IsBlockLink(n) { n.TextMarkType = strings.TrimSpace(strings.TrimPrefix(n.TextMarkType, "a") + " block-ref") n.TextMarkBlockRefID = strings.TrimPrefix(n.TextMarkAHref, "siyuan://blocks/") n.TextMarkBlockRefSubtype = "s"
blockLink2Ref(currentTree, n.TextMarkBlockRefID, depth) return ast.WalkSkipChildren } else if treenode.IsBlockRef(n) { defID, _, _ := treenode.GetBlockRef(n) blockLink2Ref(currentTree, defID, depth) } return ast.WalkContinue })}
func collectFootnotesDefs(currentTree *parse.Tree, id string, refFootnotes *[]*refAsFootnotes, depth *int) { *depth++ if 4096 < *depth { return } b := treenode.GetBlockTree(id) if nil == b { return } t, err := LoadTreeByBlockID(b.RootID) if nil != err { return }
node := treenode.GetNodeInTree(t, b.ID) if nil == node { logging.LogErrorf("not found node [%s] in tree [%s]", b.ID, t.Root.ID) return } collectFootnotesDefs0(currentTree, node, refFootnotes, depth) if ast.NodeHeading == node.Type { children := treenode.HeadingChildren(node) for _, c := range children { collectFootnotesDefs0(currentTree, c, refFootnotes, depth) } } return}
func collectFootnotesDefs0(currentTree *parse.Tree, node *ast.Node, refFootnotes *[]*refAsFootnotes, depth *int) { ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue }
if treenode.IsBlockRef(n) { defID, refText, _ := treenode.GetBlockRef(n) if nil == getRefAsFootnotes(defID, refFootnotes) { if isNodeInTree(defID, currentTree) { // 当前文档内不转换脚注,直接使用锚点哈希 https://github.com/siyuan-note/siyuan/issues/13283 return ast.WalkSkipChildren } anchorText := refText if Conf.Editor.BlockRefDynamicAnchorTextMaxLen < utf8.RuneCountInString(anchorText) { anchorText = gulu.Str.SubStr(anchorText, Conf.Editor.BlockRefDynamicAnchorTextMaxLen) + "..." } *refFootnotes = append(*refFootnotes, &refAsFootnotes{ defID: defID, refNum: strconv.Itoa(len(*refFootnotes) + 1), refAnchorText: anchorText, }) collectFootnotesDefs(currentTree, defID, refFootnotes, depth) } return ast.WalkSkipChildren } return ast.WalkContinue })}
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}
func getRefAsFootnotes(defID string, slice *[]*refAsFootnotes) *refAsFootnotes { for _, e := range *slice { if e.defID == defID { return e } } return nil}
type refAsFootnotes struct { defID string refNum string refAnchorText string}
func processFileAnnotationRef(refID string, n *ast.Node, fileAnnotationRefMode int) ast.WalkStatus { p := refID[:strings.LastIndex(refID, "/")] absPath, err := GetAssetAbsPath(p) if err != nil { logging.LogWarnf("get assets abs path by rel path [%s] failed: %s", p, err) return ast.WalkSkipChildren } sya := absPath + ".sya" syaData, err := os.ReadFile(sya) if err != nil { logging.LogErrorf("read file [%s] failed: %s", sya, err) return ast.WalkSkipChildren } syaJSON := map[string]interface{}{} if err = gulu.JSON.UnmarshalJSON(syaData, &syaJSON); err != nil { 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)
refText := n.TextMarkTextContent ext := filepath.Ext(p) file := p[7:len(p)-23-len(ext)] + ext fileAnnotationRefLink := &ast.Node{Type: ast.NodeLink} fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeOpenBracket}) if 0 == fileAnnotationRefMode { fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte(file + " - p" + pageStr + " - " + refText)}) } else { fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte(refText)}) } fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeCloseBracket}) fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeOpenParen}) dest := p + "#page=" + pageStr // https://github.com/siyuan-note/siyuan/issues/11780 fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(dest)}) fileAnnotationRefLink.AppendChild(&ast.Node{Type: ast.NodeCloseParen}) n.InsertBefore(fileAnnotationRefLink) return ast.WalkSkipChildren}
func exportPandocConvertZip(baseFolderName string, docPaths, defBlockIDs []string, pandocFrom, pandocTo, ext string) (zipPath string) { defer util.ClearPushProgress(100)
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) os.RemoveAll(exportFolder) if err := os.MkdirAll(exportFolder, 0755); err != nil { logging.LogErrorf("create export temp folder failed: %s", err) return }
exportRefMode := Conf.Export.BlockRefMode wrotePathHash := map[string]string{} assetsPathMap, err := allAssetAbsPaths() if nil != err { logging.LogWarnf("get assets abs path failed: %s", err) return }
assetsOldNew, assetsNewOld := map[string]string{}, map[string]string{} luteEngine := util.NewLute() for i, p := range docPaths { rootID := util.GetTreeID(p) tree, md, isEmpty := exportMarkdownContent(rootID, ext, exportRefMode, defBlockIDs, false) if nil == tree { continue } hPath := tree.HPath 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 if 1 == len(docPaths) { // 如果仅导出单个文档则使用文档标题作为文件名,不使用父路径 https://github.com/siyuan-note/siyuan/issues/13635#issuecomment-3794560233 p = name + ext }
writePath := filepath.Join(exportFolder, p) hash := fmt.Sprintf("%x", sha1.Sum([]byte(md))) if gulu.File.IsExist(writePath) && hash != wrotePathHash[writePath] { // 重名文档加 ID p = hPath + "-" + rootID + ext writePath = filepath.Join(exportFolder, p) } writeFolder := filepath.Dir(writePath) if err := os.MkdirAll(writeFolder, 0755); err != nil { logging.LogErrorf("create export temp folder [%s] failed: %s", writeFolder, err) continue }
if isEmpty { entries, readErr := os.ReadDir(filepath.Join(util.DataDir, tree.Box, strings.TrimSuffix(tree.Path, ".sy"))) if nil == readErr && 0 < len(entries) { // 如果文档内容为空并且存在子文档则仅导出文件夹 // Improve export of empty documents with subdocuments https://github.com/siyuan-note/siyuan/issues/15009 continue } }
// 解析导出后的标准 Markdown,汇总 assets tree = parse.Parse("", gulu.Str.ToBytes(md), luteEngine.ParseOptions) removeAssetsID(tree, assetsOldNew, assetsNewOld)
newAssets := getAssetsLinkDests(tree.Root, false) for _, newAsset := range newAssets { newAsset = string(html.DecodeDestination([]byte(newAsset))) if strings.Contains(newAsset, "?") { newAsset = newAsset[:strings.LastIndex(newAsset, "?")] }
if !strings.HasPrefix(newAsset, "assets/") { continue }
// 导出 Markdown 时链接路径中的空格被编码为 `%20`,需要替换回空格后才能正确获取原始资源路径 // Improve export of Markdown hyperlink spaces https://github.com/siyuan-note/siyuan/issues/9792 // No assets were exported when exporting Markdown https://github.com/siyuan-note/siyuan/issues/17046 spaceEncodedNewAsset := strings.ReplaceAll(newAsset, " ", "%20") oldAsset := assetsNewOld[spaceEncodedNewAsset] if "" == oldAsset { logging.LogWarnf("get asset old path for new asset [%s] failed", spaceEncodedNewAsset) continue }
spaceDecodedOldAsset := strings.ReplaceAll(oldAsset, "%20", " ") srcPath := assetsPathMap[spaceDecodedOldAsset] if "" == srcPath { logging.LogWarnf("get asset [%s] abs path failed", spaceDecodedOldAsset) continue }
destPath := filepath.Join(writeFolder, newAsset) if copyErr := filelock.Copy(srcPath, destPath); copyErr != nil { logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", srcPath, destPath, err) continue } }
for assetsOld, assetsNew := range assetsOldNew { md = strings.ReplaceAll(md, assetsOld, assetsNew) }
// 调用 Pandoc 进行格式转换 pandocErr := util.Pandoc(pandocFrom, pandocTo, writePath, md) if pandocErr != nil { logging.LogErrorf("pandoc failed: %s", pandocErr) continue }
wrotePathHash[writePath] = hash util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(70), fmt.Sprintf("%d/%d %s", i+1, len(docPaths), name))) }
zipPath = exportFolder + ".zip" zip, err := gulu.Zip.Create(zipPath) if err != nil { 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) if err != nil { logging.LogErrorf("read export markdown folder [%s] failed: %s", exportFolder, err) return "" }
zipCallback := func(filename string) { util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(253), filename)) } for _, entry := range entries { entryName := entry.Name() entryPath := filepath.Join(exportFolder, entryName) if gulu.File.IsDir(entryPath) { err = zip.AddDirectory(entryName, entryPath, zipCallback) } else { err = zip.AddEntry(entryName, entryPath, zipCallback) } if err != nil { logging.LogErrorf("add entry [%s] to zip failed: %s", entryName, err) return "" } }
if err = zip.Close(); err != nil { logging.LogErrorf("close export markdown zip failed: %s", err) }
os.RemoveAll(exportFolder) zipPath = "/export/" + url.PathEscape(filepath.Base(zipPath)) return}
func removeAssetsID(tree *parse.Tree, assetsOldNew, assetsNewOld map[string]string) { assetNodes := getAssetsLinkDestsInTree(tree, false) for _, node := range assetNodes { dests := getAssetsLinkDests(node, false) if 1 > len(dests) { continue }
for _, dest := range dests { if !Conf.Export.RemoveAssetsID { assetsOldNew[dest] = dest assetsNewOld[dest] = dest continue }
if newDest := assetsOldNew[dest]; "" != newDest { setAssetsLinkDest(node, dest, newDest) continue }
name := path.Base(dest) name = util.RemoveID(name) newDest := "assets/" + name if existOld := assetsNewOld[newDest]; "" != existOld { if existOld == dest { // 已存在相同资源路径 setAssetsLinkDest(node, dest, newDest) } else { // 存在同名但内容不同的资源文件,保留 ID assetsNewOld[dest] = dest assetsOldNew[dest] = dest } continue }
setAssetsLinkDest(node, dest, newDest) assetsOldNew[dest] = newDest assetsNewOld[newDest] = dest } }}
func getExportBlockRefLinkText(blockRef *ast.Node, blockRefTextLeft, blockRefTextRight string) (defID, linkText string) { defID, linkText, _ = treenode.GetBlockRef(blockRef) if "" == linkText { linkText = sql.GetRefText(defID) } linkText = util.UnescapeHTML(linkText) // 块引锚文本导出时 `&` 变为实体 `&` https://github.com/siyuan-note/siyuan/issues/7659 if Conf.Editor.BlockRefDynamicAnchorTextMaxLen < utf8.RuneCountInString(linkText) { linkText = gulu.Str.SubStr(linkText, Conf.Editor.BlockRefDynamicAnchorTextMaxLen) + "..." } linkText = blockRefTextLeft + linkText + blockRefTextRight return}
func prepareExportTrees(docPaths []string) (defBlockIDs []string, relatedDocPaths []string) { trees := map[string]*parse.Tree{} defBlockIDs = []string{} for i, p := range docPaths { rootID := strings.TrimSuffix(path.Base(p), ".sy") if !ast.IsNodeIDPattern(rootID) { continue }
tree, err := LoadTreeByBlockID(rootID) if err != nil { continue } exportRefTrees(tree, &defBlockIDs, trees)
util.PushEndlessProgress(Conf.language(65) + " " + fmt.Sprintf(Conf.language(70), fmt.Sprintf("%d/%d %s", i+1, len(docPaths), tree.Root.IALAttr("title")))) }
for _, tree := range trees { relatedDocPaths = append(relatedDocPaths, tree.Path) } relatedDocPaths = gulu.Str.RemoveDuplicatedElem(relatedDocPaths) return}
func exportRefTrees(tree *parse.Tree, defBlockIDs *[]string, retTrees map[string]*parse.Tree) { if nil != retTrees[tree.ID] { return } retTrees[tree.ID] = tree
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering { return ast.WalkContinue }
if treenode.IsBlockRef(n) { defID, _, _ := treenode.GetBlockRef(n) if "" == defID { return ast.WalkContinue } defBlock := treenode.GetBlockTree(defID) if nil == defBlock { return ast.WalkSkipChildren }
defTree, err := LoadTreeByBlockID(defBlock.RootID) if err != nil { return ast.WalkSkipChildren } *defBlockIDs = append(*defBlockIDs, defID)
if !Conf.Export.IncludeRelatedDocs { return ast.WalkSkipChildren } exportRefTrees(defTree, defBlockIDs, retTrees) } 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 }
defTree, err := LoadTreeByBlockID(defBlock.RootID) if err != nil { return ast.WalkSkipChildren } *defBlockIDs = append(*defBlockIDs, defID)
if !Conf.Export.IncludeRelatedDocs { return ast.WalkSkipChildren } exportRefTrees(defTree, defBlockIDs, retTrees) } 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 { if val.IsDetached { continue }
defBlock := treenode.GetBlockTree(val.Block.ID) if nil == defBlock { continue }
defTree, err := LoadTreeByBlockID(defBlock.RootID) if err != nil { continue } *defBlockIDs = append(*defBlockIDs, val.BlockID)
if !Conf.Export.IncludeRelatedDocs { return ast.WalkSkipChildren } exportRefTrees(defTree, defBlockIDs, retTrees) } } return ast.WalkContinue })
*defBlockIDs = gulu.Str.RemoveDuplicatedElem(*defBlockIDs)}
func getAttrViewTable(attrView *av.AttributeView, view *av.View, query string) (ret *av.Table) { switch view.LayoutType { case av.LayoutTypeGallery: view.Table = av.NewLayoutTable() for _, field := range view.Gallery.CardFields { view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{BaseField: &av.BaseField{ID: field.ID}}) } case av.LayoutTypeKanban: view.Table = av.NewLayoutTable() for _, field := range view.Kanban.Fields { view.Table.Columns = append(view.Table.Columns, &av.ViewTableColumn{BaseField: &av.BaseField{ID: field.ID}}) } }
depth := 1 ret = sql.RenderAttributeViewTable(attrView, view, query, &depth, map[string]*av.AttributeView{}) return}
// adjustHeadingLevel 聚焦导出(即非文档块)的情况下,将第一个标题层级提升为一级(如果开启了添加文档标题的话提升为二级)。// Export preview mode supports focus use https://github.com/siyuan-note/siyuan/issues/15340func adjustHeadingLevel(bt *treenode.BlockTree, tree *parse.Tree) { if "d" == bt.Type { return }
level := 1 var firstHeading *ast.Node if !Conf.Export.AddTitle { for n := tree.Root.FirstChild; nil != n; n = n.Next { if ast.NodeHeading == n.Type && !n.ParentIs(ast.NodeBlockquote) && !n.ParentIs(ast.NodeCallout) { firstHeading = n break } } } else { for n := tree.Root.FirstChild.Next; nil != n; n = n.Next { if ast.NodeHeading == n.Type && !n.ParentIs(ast.NodeBlockquote) && !n.ParentIs(ast.NodeCallout) { firstHeading = n break } } level = 2 } if nil != firstHeading { hLevel := firstHeading.HeadingLevel diff := level - hLevel var children, childrenHeadings []*ast.Node children = append(children, firstHeading) children = append(children, treenode.HeadingChildren(firstHeading)...) for _, c := range children { ccH := c.ChildrenByType(ast.NodeHeading) childrenHeadings = append(childrenHeadings, ccH...) } for _, h := range childrenHeadings { h.HeadingLevel += diff if 6 < h.HeadingLevel { h.HeadingLevel = 6 } else if 1 > h.HeadingLevel { h.HeadingLevel = 1 } } }}