file.go 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103
  1. // SiYuan - Refactor your thinking
  2. // Copyright (c) 2020-present, b3log.org
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. package model
  17. import (
  18. "bytes"
  19. "errors"
  20. "fmt"
  21. "os"
  22. "path"
  23. "path/filepath"
  24. "sort"
  25. "strconv"
  26. "strings"
  27. "sync"
  28. "time"
  29. "unicode/utf8"
  30. "github.com/88250/go-humanize"
  31. "github.com/88250/gulu"
  32. "github.com/88250/lute"
  33. "github.com/88250/lute/ast"
  34. "github.com/88250/lute/html"
  35. "github.com/88250/lute/parse"
  36. util2 "github.com/88250/lute/util"
  37. "github.com/facette/natsort"
  38. "github.com/siyuan-note/filelock"
  39. "github.com/siyuan-note/logging"
  40. "github.com/siyuan-note/riff"
  41. "github.com/siyuan-note/siyuan/kernel/av"
  42. "github.com/siyuan-note/siyuan/kernel/cache"
  43. "github.com/siyuan-note/siyuan/kernel/filesys"
  44. "github.com/siyuan-note/siyuan/kernel/search"
  45. "github.com/siyuan-note/siyuan/kernel/sql"
  46. "github.com/siyuan-note/siyuan/kernel/task"
  47. "github.com/siyuan-note/siyuan/kernel/treenode"
  48. "github.com/siyuan-note/siyuan/kernel/util"
  49. )
  50. type File struct {
  51. Path string `json:"path"`
  52. Name string `json:"name"` // 标题,即 ial["title"]
  53. Icon string `json:"icon"`
  54. Name1 string `json:"name1"` // 命名,即 ial["name"]
  55. Alias string `json:"alias"`
  56. Memo string `json:"memo"`
  57. Bookmark string `json:"bookmark"`
  58. ID string `json:"id"`
  59. Count int `json:"count"`
  60. Size uint64 `json:"size"`
  61. HSize string `json:"hSize"`
  62. Mtime int64 `json:"mtime"`
  63. CTime int64 `json:"ctime"`
  64. HMtime string `json:"hMtime"`
  65. HCtime string `json:"hCtime"`
  66. Sort int `json:"sort"`
  67. SubFileCount int `json:"subFileCount"`
  68. Hidden bool `json:"hidden"`
  69. NewFlashcardCount int `json:"newFlashcardCount"`
  70. DueFlashcardCount int `json:"dueFlashcardCount"`
  71. FlashcardCount int `json:"flashcardCount"`
  72. }
  73. func (box *Box) docFromFileInfo(fileInfo *FileInfo, ial map[string]string) (ret *File) {
  74. ret = &File{}
  75. ret.Path = fileInfo.path
  76. ret.Size = uint64(fileInfo.size)
  77. ret.Name = ial["title"] + ".sy"
  78. ret.Icon = ial["icon"]
  79. ret.ID = ial["id"]
  80. ret.Name1 = ial["name"]
  81. ret.Alias = ial["alias"]
  82. ret.Memo = ial["memo"]
  83. ret.Bookmark = ial["bookmark"]
  84. t, _ := time.ParseInLocation("20060102150405", ret.ID[:14], time.Local)
  85. ret.CTime = t.Unix()
  86. ret.HCtime = t.Format("2006-01-02 15:04:05") + ", " + util.HumanizeTime(t, Conf.Lang)
  87. ret.HSize = humanize.BytesCustomCeil(ret.Size, 2)
  88. mTime := t
  89. if updated := ial["updated"]; "" != updated {
  90. if updatedTime, err := time.ParseInLocation("20060102150405", updated, time.Local); nil == err {
  91. mTime = updatedTime
  92. }
  93. }
  94. ret.Mtime = mTime.Unix()
  95. ret.HMtime = mTime.Format("2006-01-02 15:04:05") + ", " + util.HumanizeTime(mTime, Conf.Lang)
  96. return
  97. }
  98. func (box *Box) docIAL(p string) (ret map[string]string) {
  99. name := strings.ToLower(filepath.Base(p))
  100. if !strings.HasSuffix(name, ".sy") {
  101. return nil
  102. }
  103. ret = cache.GetDocIAL(p)
  104. if nil != ret {
  105. return ret
  106. }
  107. filePath := filepath.Join(util.DataDir, box.ID, p)
  108. data, err := filelock.ReadFile(filePath)
  109. if util.IsCorruptedSYData(data) {
  110. box.moveCorruptedData(filePath)
  111. return nil
  112. }
  113. if nil != err {
  114. logging.LogErrorf("read file [%s] failed: %s", p, err)
  115. return nil
  116. }
  117. ret = filesys.ReadDocIAL(data)
  118. if 1 > len(ret) {
  119. logging.LogWarnf("tree [%s] is corrupted", filePath)
  120. box.moveCorruptedData(filePath)
  121. return nil
  122. }
  123. cache.PutDocIAL(p, ret)
  124. return ret
  125. }
  126. func (box *Box) moveCorruptedData(filePath string) {
  127. base := filepath.Base(filePath)
  128. to := filepath.Join(util.WorkspaceDir, "corrupted", time.Now().Format("2006-01-02-150405"), box.ID, base)
  129. if copyErr := filelock.Copy(filePath, to); nil != copyErr {
  130. logging.LogErrorf("copy corrupted data file [%s] failed: %s", filePath, copyErr)
  131. return
  132. }
  133. if removeErr := filelock.Remove(filePath); nil != removeErr {
  134. logging.LogErrorf("remove corrupted data file [%s] failed: %s", filePath, removeErr)
  135. return
  136. }
  137. logging.LogWarnf("moved corrupted data file [%s] to [%s]", filePath, to)
  138. }
  139. func SearchDocsByKeyword(keyword string, flashcard bool) (ret []map[string]string) {
  140. ret = []map[string]string{}
  141. var deck *riff.Deck
  142. var deckBlockIDs []string
  143. if flashcard {
  144. deck = Decks[builtinDeckID]
  145. if nil == deck {
  146. return
  147. }
  148. deckBlockIDs = deck.GetBlockIDs()
  149. }
  150. openedBoxes := Conf.GetOpenedBoxes()
  151. boxes := map[string]*Box{}
  152. for _, box := range openedBoxes {
  153. boxes[box.ID] = box
  154. }
  155. var rootBlocks []*sql.Block
  156. if "" != keyword {
  157. for _, box := range boxes {
  158. if strings.Contains(box.Name, keyword) {
  159. if flashcard {
  160. newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs)
  161. if 0 < flashcardCount {
  162. ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon, "newFlashcardCount": strconv.Itoa(newFlashcardCount), "dueFlashcardCount": strconv.Itoa(dueFlashcardCount), "flashcardCount": strconv.Itoa(flashcardCount)})
  163. }
  164. } else {
  165. ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon})
  166. }
  167. }
  168. }
  169. condition := "hpath LIKE '%" + keyword + "%'"
  170. if "" != keyword {
  171. namCondition := Conf.Search.NAMFilter(keyword)
  172. if "" != namCondition {
  173. condition += " " + namCondition
  174. }
  175. }
  176. rootBlocks = sql.QueryRootBlockByCondition(condition)
  177. } else {
  178. for _, box := range boxes {
  179. if flashcard {
  180. newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs)
  181. if 0 < flashcardCount {
  182. ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon, "newFlashcardCount": strconv.Itoa(newFlashcardCount), "dueFlashcardCount": strconv.Itoa(dueFlashcardCount), "flashcardCount": strconv.Itoa(flashcardCount)})
  183. }
  184. } else {
  185. ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon})
  186. }
  187. }
  188. }
  189. for _, rootBlock := range rootBlocks {
  190. b := boxes[rootBlock.Box]
  191. if nil == b {
  192. continue
  193. }
  194. hPath := b.Name + rootBlock.HPath
  195. if flashcard {
  196. newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootBlock.ID, deck, deckBlockIDs)
  197. if 0 < flashcardCount {
  198. ret = append(ret, map[string]string{"path": rootBlock.Path, "hPath": hPath, "box": rootBlock.Box, "boxIcon": b.Icon, "newFlashcardCount": strconv.Itoa(newFlashcardCount), "dueFlashcardCount": strconv.Itoa(dueFlashcardCount), "flashcardCount": strconv.Itoa(flashcardCount)})
  199. }
  200. } else {
  201. ret = append(ret, map[string]string{"path": rootBlock.Path, "hPath": hPath, "box": rootBlock.Box, "boxIcon": b.Icon})
  202. }
  203. }
  204. sort.Slice(ret, func(i, j int) bool {
  205. return ret[i]["hPath"] < ret[j]["hPath"]
  206. })
  207. return
  208. }
  209. type FileInfo struct {
  210. path string
  211. name string
  212. size int64
  213. isdir bool
  214. }
  215. func ListDocTree(boxID, listPath string, sortMode int, flashcard, showHidden bool, maxListCount int) (ret []*File, totals int, err error) {
  216. //os.MkdirAll("pprof", 0755)
  217. //cpuProfile, _ := os.Create("pprof/cpu_profile_list_doc_tree")
  218. //pprof.StartCPUProfile(cpuProfile)
  219. //defer pprof.StopCPUProfile()
  220. ret = []*File{}
  221. var deck *riff.Deck
  222. var deckBlockIDs []string
  223. if flashcard {
  224. deck = Decks[builtinDeckID]
  225. if nil == deck {
  226. return
  227. }
  228. deckBlockIDs = deck.GetBlockIDs()
  229. }
  230. box := Conf.Box(boxID)
  231. if nil == box {
  232. return nil, 0, errors.New(Conf.Language(0))
  233. }
  234. boxConf := box.GetConf()
  235. if util.SortModeUnassigned == sortMode {
  236. sortMode = Conf.FileTree.Sort
  237. if util.SortModeFileTree != boxConf.SortMode {
  238. sortMode = boxConf.SortMode
  239. }
  240. }
  241. var files []*FileInfo
  242. start := time.Now()
  243. files, totals, err = box.Ls(listPath)
  244. if nil != err {
  245. return
  246. }
  247. elapsed := time.Now().Sub(start).Milliseconds()
  248. if 100 < elapsed {
  249. logging.LogWarnf("ls elapsed [%dms]", elapsed)
  250. }
  251. start = time.Now()
  252. boxLocalPath := filepath.Join(util.DataDir, box.ID)
  253. var docs []*File
  254. for _, file := range files {
  255. if file.isdir {
  256. if !ast.IsNodeIDPattern(file.name) {
  257. continue
  258. }
  259. parentDocPath := strings.TrimSuffix(file.path, "/") + ".sy"
  260. parentDocFile := box.Stat(parentDocPath)
  261. if nil == parentDocFile {
  262. continue
  263. }
  264. if ial := box.docIAL(parentDocPath); nil != ial {
  265. if !showHidden && "true" == ial["custom-hidden"] {
  266. continue
  267. }
  268. doc := box.docFromFileInfo(parentDocFile, ial)
  269. subFiles, err := os.ReadDir(filepath.Join(boxLocalPath, file.path))
  270. if nil == err {
  271. for _, subFile := range subFiles {
  272. subDocFilePath := path.Join(file.path, subFile.Name())
  273. if subIAL := box.docIAL(subDocFilePath); "true" == subIAL["custom-hidden"] {
  274. continue
  275. }
  276. if strings.HasSuffix(subFile.Name(), ".sy") {
  277. doc.SubFileCount++
  278. }
  279. }
  280. }
  281. if flashcard {
  282. rootID := strings.TrimSuffix(filepath.Base(parentDocPath), ".sy")
  283. newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootID, deck, deckBlockIDs)
  284. if 0 < flashcardCount {
  285. doc.NewFlashcardCount = newFlashcardCount
  286. doc.DueFlashcardCount = dueFlashcardCount
  287. doc.FlashcardCount = flashcardCount
  288. docs = append(docs, doc)
  289. }
  290. } else {
  291. docs = append(docs, doc)
  292. }
  293. }
  294. continue
  295. }
  296. subFolder := filepath.Join(boxLocalPath, strings.TrimSuffix(file.path, ".sy"))
  297. if gulu.File.IsDir(subFolder) {
  298. continue
  299. }
  300. if ial := box.docIAL(file.path); nil != ial {
  301. if !showHidden && "true" == ial["custom-hidden"] {
  302. continue
  303. }
  304. doc := box.docFromFileInfo(file, ial)
  305. if flashcard {
  306. rootID := strings.TrimSuffix(filepath.Base(file.path), ".sy")
  307. newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootID, deck, deckBlockIDs)
  308. if 0 < flashcardCount {
  309. doc.NewFlashcardCount = newFlashcardCount
  310. doc.DueFlashcardCount = dueFlashcardCount
  311. doc.FlashcardCount = flashcardCount
  312. docs = append(docs, doc)
  313. }
  314. } else {
  315. docs = append(docs, doc)
  316. }
  317. }
  318. }
  319. elapsed = time.Now().Sub(start).Milliseconds()
  320. if 500 < elapsed {
  321. logging.LogWarnf("build docs [%d] elapsed [%dms]", len(docs), elapsed)
  322. }
  323. start = time.Now()
  324. refCount := sql.QueryRootBlockRefCount()
  325. for _, doc := range docs {
  326. if count := refCount[doc.ID]; 0 < count {
  327. doc.Count = count
  328. }
  329. }
  330. elapsed = time.Now().Sub(start).Milliseconds()
  331. if 500 < elapsed {
  332. logging.LogWarnf("query root block ref count elapsed [%dms]", elapsed)
  333. }
  334. start = time.Now()
  335. switch sortMode {
  336. case util.SortModeNameASC:
  337. sort.Slice(docs, func(i, j int) bool {
  338. return util.PinYinCompare(util.RemoveEmojiInvisible(docs[i].Name), util.RemoveEmojiInvisible(docs[j].Name))
  339. })
  340. case util.SortModeNameDESC:
  341. sort.Slice(docs, func(i, j int) bool {
  342. return util.PinYinCompare(util.RemoveEmojiInvisible(docs[j].Name), util.RemoveEmojiInvisible(docs[i].Name))
  343. })
  344. case util.SortModeUpdatedASC:
  345. sort.Slice(docs, func(i, j int) bool { return docs[i].Mtime < docs[j].Mtime })
  346. case util.SortModeUpdatedDESC:
  347. sort.Slice(docs, func(i, j int) bool { return docs[i].Mtime > docs[j].Mtime })
  348. case util.SortModeAlphanumASC:
  349. sort.Slice(docs, func(i, j int) bool {
  350. return natsort.Compare(util.RemoveEmojiInvisible(docs[i].Name), util.RemoveEmojiInvisible(docs[j].Name))
  351. })
  352. case util.SortModeAlphanumDESC:
  353. sort.Slice(docs, func(i, j int) bool {
  354. return natsort.Compare(util.RemoveEmojiInvisible(docs[j].Name), util.RemoveEmojiInvisible(docs[i].Name))
  355. })
  356. case util.SortModeCustom:
  357. fileTreeFiles := docs
  358. box.fillSort(&fileTreeFiles)
  359. sort.Slice(fileTreeFiles, func(i, j int) bool {
  360. if fileTreeFiles[i].Sort == fileTreeFiles[j].Sort {
  361. return util.TimeFromID(fileTreeFiles[i].ID) > util.TimeFromID(fileTreeFiles[j].ID)
  362. }
  363. return fileTreeFiles[i].Sort < fileTreeFiles[j].Sort
  364. })
  365. ret = append(ret, fileTreeFiles...)
  366. totals = len(ret)
  367. if maxListCount < len(ret) {
  368. ret = ret[:maxListCount]
  369. }
  370. ret = ret[:]
  371. return
  372. case util.SortModeRefCountASC:
  373. sort.Slice(docs, func(i, j int) bool { return docs[i].Count < docs[j].Count })
  374. case util.SortModeRefCountDESC:
  375. sort.Slice(docs, func(i, j int) bool { return docs[i].Count > docs[j].Count })
  376. case util.SortModeCreatedASC:
  377. sort.Slice(docs, func(i, j int) bool { return docs[i].CTime < docs[j].CTime })
  378. case util.SortModeCreatedDESC:
  379. sort.Slice(docs, func(i, j int) bool { return docs[i].CTime > docs[j].CTime })
  380. case util.SortModeSizeASC:
  381. sort.Slice(docs, func(i, j int) bool { return docs[i].Size < docs[j].Size })
  382. case util.SortModeSizeDESC:
  383. sort.Slice(docs, func(i, j int) bool { return docs[i].Size > docs[j].Size })
  384. case util.SortModeSubDocCountASC:
  385. sort.Slice(docs, func(i, j int) bool { return docs[i].SubFileCount < docs[j].SubFileCount })
  386. case util.SortModeSubDocCountDESC:
  387. sort.Slice(docs, func(i, j int) bool { return docs[i].SubFileCount > docs[j].SubFileCount })
  388. }
  389. if util.SortModeCustom != sortMode {
  390. ret = append(ret, docs...)
  391. }
  392. totals = len(ret)
  393. if maxListCount < len(ret) {
  394. ret = ret[:maxListCount]
  395. }
  396. ret = ret[:]
  397. elapsed = time.Now().Sub(start).Milliseconds()
  398. if 200 < elapsed {
  399. logging.LogInfof("sort docs elapsed [%dms]", elapsed)
  400. }
  401. return
  402. }
  403. func ContentStat(content string) (ret *util.BlockStatResult) {
  404. luteEngine := util.NewLute()
  405. return contentStat(content, luteEngine)
  406. }
  407. func contentStat(content string, luteEngine *lute.Lute) (ret *util.BlockStatResult) {
  408. tree := luteEngine.BlockDOM2Tree(content)
  409. runeCnt, wordCnt, linkCnt, imgCnt, refCnt := tree.Root.Stat()
  410. return &util.BlockStatResult{
  411. RuneCount: runeCnt,
  412. WordCount: wordCnt,
  413. LinkCount: linkCnt,
  414. ImageCount: imgCnt,
  415. RefCount: refCnt,
  416. }
  417. }
  418. func BlocksWordCount(ids []string) (ret *util.BlockStatResult) {
  419. ret = &util.BlockStatResult{}
  420. trees := map[string]*parse.Tree{} // 缓存
  421. luteEngine := util.NewLute()
  422. for _, id := range ids {
  423. bt := treenode.GetBlockTree(id)
  424. if nil == bt {
  425. continue
  426. }
  427. tree := trees[bt.RootID]
  428. if nil == tree {
  429. tree, _ = filesys.LoadTree(bt.BoxID, bt.Path, luteEngine)
  430. if nil == tree {
  431. continue
  432. }
  433. trees[bt.RootID] = tree
  434. }
  435. node := treenode.GetNodeInTree(tree, id)
  436. if nil == node {
  437. continue
  438. }
  439. runeCnt, wordCnt, linkCnt, imgCnt, refCnt := node.Stat()
  440. ret.RuneCount += runeCnt
  441. ret.WordCount += wordCnt
  442. ret.LinkCount += linkCnt
  443. ret.ImageCount += imgCnt
  444. ret.RefCount += refCnt
  445. }
  446. return
  447. }
  448. func StatTree(id string) (ret *util.BlockStatResult) {
  449. WaitForWritingFiles()
  450. tree, _ := LoadTreeByBlockID(id)
  451. if nil == tree {
  452. return
  453. }
  454. var databaseBlockNodes []*ast.Node
  455. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  456. if !entering || ast.NodeAttributeView != n.Type {
  457. return ast.WalkContinue
  458. }
  459. databaseBlockNodes = append(databaseBlockNodes, n)
  460. return ast.WalkContinue
  461. })
  462. luteEngine := util.NewLute()
  463. var dbRuneCnt, dbWordCnt, dbLinkCnt, dbImgCnt, dbRefCnt int
  464. for _, n := range databaseBlockNodes {
  465. if "" == n.AttributeViewID {
  466. continue
  467. }
  468. attrView, _ := av.ParseAttributeView(n.AttributeViewID)
  469. if nil == attrView {
  470. continue
  471. }
  472. content := bytes.Buffer{}
  473. for _, kValues := range attrView.KeyValues {
  474. for _, v := range kValues.Values {
  475. switch kValues.Key.Type {
  476. case av.KeyTypeURL:
  477. if v.IsEmpty() {
  478. continue
  479. }
  480. dbLinkCnt++
  481. content.WriteString(v.URL.Content)
  482. case av.KeyTypeMAsset:
  483. if v.IsEmpty() {
  484. continue
  485. }
  486. for _, asset := range v.MAsset {
  487. if av.AssetTypeImage == asset.Type {
  488. dbImgCnt++
  489. }
  490. }
  491. case av.KeyTypeBlock:
  492. if v.IsEmpty() {
  493. continue
  494. }
  495. if !v.IsDetached {
  496. dbRefCnt++
  497. }
  498. content.WriteString(v.Block.Content)
  499. case av.KeyTypeText:
  500. if v.IsEmpty() {
  501. continue
  502. }
  503. content.WriteString(v.Text.Content)
  504. case av.KeyTypeNumber:
  505. if v.IsEmpty() {
  506. continue
  507. }
  508. v.Number.FormatNumber()
  509. content.WriteString(v.Number.FormattedContent)
  510. case av.KeyTypeEmail:
  511. if v.IsEmpty() {
  512. continue
  513. }
  514. content.WriteString(v.Email.Content)
  515. case av.KeyTypePhone:
  516. if v.IsEmpty() {
  517. continue
  518. }
  519. content.WriteString(v.Phone.Content)
  520. }
  521. }
  522. }
  523. dbStat := contentStat(content.String(), luteEngine)
  524. dbRuneCnt += dbStat.RuneCount
  525. dbWordCnt += dbStat.WordCount
  526. }
  527. runeCnt, wordCnt, linkCnt, imgCnt, refCnt := tree.Root.Stat()
  528. runeCnt += dbRuneCnt
  529. wordCnt += dbWordCnt
  530. linkCnt += dbLinkCnt
  531. imgCnt += dbImgCnt
  532. refCnt += dbRefCnt
  533. return &util.BlockStatResult{
  534. RuneCount: runeCnt,
  535. WordCount: wordCnt,
  536. LinkCount: linkCnt,
  537. ImageCount: imgCnt,
  538. RefCount: refCnt,
  539. }
  540. }
  541. func GetDoc(startID, endID, id string, index int, query string, queryTypes map[string]bool, queryMethod, mode int, size int, isBacklink bool) (blockCount int, dom, parentID, parent2ID, rootID, typ string, eof, scroll bool, boxID, docPath string, isBacklinkExpand bool, err error) {
  542. //os.MkdirAll("pprof", 0755)
  543. //cpuProfile, _ := os.Create("pprof/GetDoc")
  544. //pprof.StartCPUProfile(cpuProfile)
  545. //defer pprof.StopCPUProfile()
  546. WaitForWritingFiles() // 写入数据时阻塞,避免获取到的数据不一致
  547. inputIndex := index
  548. tree, err := LoadTreeByBlockID(id)
  549. if nil != err {
  550. if ErrBlockNotFound == err {
  551. if 0 == mode {
  552. err = ErrTreeNotFound // 初始化打开文档时如果找不到则关闭编辑器
  553. }
  554. }
  555. return
  556. }
  557. if nil == tree {
  558. err = ErrBlockNotFound
  559. return
  560. }
  561. luteEngine := NewLute()
  562. node := treenode.GetNodeInTree(tree, id)
  563. if nil == node {
  564. // Unable to open the doc when the block pointed by the scroll position does not exist https://github.com/siyuan-note/siyuan/issues/9030
  565. node = treenode.GetNodeInTree(tree, tree.Root.ID)
  566. if nil == node {
  567. err = ErrBlockNotFound
  568. return
  569. }
  570. }
  571. if isBacklink { // 引用计数浮窗请求,需要按照反链逻辑组装 https://github.com/siyuan-note/siyuan/issues/6853
  572. if ast.NodeParagraph == node.Type {
  573. if nil != node.Parent && ast.NodeListItem == node.Parent.Type {
  574. node = node.Parent
  575. }
  576. }
  577. }
  578. located := false
  579. isDoc := ast.NodeDocument == node.Type
  580. isHeading := ast.NodeHeading == node.Type
  581. boxID = node.Box
  582. docPath = node.Path
  583. if isDoc {
  584. if 4 == mode { // 加载文档末尾
  585. node = node.LastChild
  586. located = true
  587. // 重新计算 index
  588. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  589. if !entering {
  590. return ast.WalkContinue
  591. }
  592. index++
  593. return ast.WalkContinue
  594. })
  595. } else {
  596. node = node.FirstChild
  597. }
  598. typ = ast.NodeDocument.String()
  599. idx := 0
  600. if 0 < index {
  601. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  602. if !entering || !n.IsChildBlockOf(tree.Root, 1) {
  603. return ast.WalkContinue
  604. }
  605. idx++
  606. if index == idx {
  607. node = n.DocChild()
  608. if "1" == node.IALAttr("heading-fold") {
  609. // 加载到折叠标题下方块的话需要回溯到上方标题块
  610. for h := node.Previous; nil != h; h = h.Previous {
  611. if "1" == h.IALAttr("fold") {
  612. node = h
  613. break
  614. }
  615. }
  616. }
  617. located = true
  618. return ast.WalkStop
  619. }
  620. return ast.WalkContinue
  621. })
  622. }
  623. } else {
  624. if 0 == index && 0 != mode {
  625. // 非文档且没有指定 index 时需要计算 index
  626. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  627. if !entering {
  628. return ast.WalkContinue
  629. }
  630. index++
  631. if id == n.ID {
  632. node = n.DocChild()
  633. located = true
  634. return ast.WalkStop
  635. }
  636. return ast.WalkContinue
  637. })
  638. }
  639. }
  640. if 1 < index && !located {
  641. count := 0
  642. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  643. if !entering {
  644. return ast.WalkContinue
  645. }
  646. count++
  647. if index == count {
  648. node = n.DocChild()
  649. return ast.WalkStop
  650. }
  651. return ast.WalkContinue
  652. })
  653. }
  654. blockCount = tree.DocBlockCount()
  655. if ast.NodeDocument == node.Type {
  656. parentID = node.ID
  657. parent2ID = parentID
  658. } else {
  659. parentID = node.Parent.ID
  660. parent2ID = parentID
  661. tmp := node
  662. if ast.NodeListItem == node.Type {
  663. // 列表项聚焦返回和面包屑保持一致 https://github.com/siyuan-note/siyuan/issues/4914
  664. tmp = node.Parent
  665. }
  666. if headingParent := treenode.HeadingParent(tmp); nil != headingParent {
  667. parent2ID = headingParent.ID
  668. }
  669. }
  670. rootID = tree.Root.ID
  671. if !isDoc {
  672. typ = node.Type.String()
  673. }
  674. // 判断是否需要显示动态加载滚动条 https://github.com/siyuan-note/siyuan/issues/7693
  675. childCount := 0
  676. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  677. if !entering {
  678. return ast.WalkContinue
  679. }
  680. if 1 > childCount {
  681. childCount = 1
  682. } else {
  683. childCount += treenode.CountBlockNodes(n)
  684. }
  685. if childCount > Conf.Editor.DynamicLoadBlocks {
  686. scroll = true
  687. return ast.WalkStop
  688. }
  689. return ast.WalkContinue
  690. })
  691. var nodes []*ast.Node
  692. if isBacklink {
  693. // 引用计数浮窗请求,需要按照反链逻辑组装 https://github.com/siyuan-note/siyuan/issues/6853
  694. nodes, isBacklinkExpand = getBacklinkRenderNodes(node)
  695. } else {
  696. // 如果同时存在 startID 和 endID,并且是动态加载的情况,则只加载 startID 和 endID 之间的块 [startID, endID]
  697. if "" != startID && "" != endID && scroll {
  698. nodes, eof = loadNodesByStartEnd(tree, startID, endID)
  699. if 1 > len(nodes) {
  700. // 按 mode 加载兜底
  701. nodes, eof = loadNodesByMode(node, inputIndex, mode, size, isDoc, isHeading)
  702. } else {
  703. // 文档块没有指定 index 时需要计算 index,否则初次打开文档时 node-index 会为 0,导致首次 Ctrl+Home 无法回到顶部
  704. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  705. if !entering {
  706. return ast.WalkContinue
  707. }
  708. index++
  709. if nodes[0].ID == n.ID {
  710. return ast.WalkStop
  711. }
  712. return ast.WalkContinue
  713. })
  714. }
  715. } else {
  716. nodes, eof = loadNodesByMode(node, inputIndex, mode, size, isDoc, isHeading)
  717. }
  718. }
  719. refCount := sql.QueryRootChildrenRefCount(rootID)
  720. virtualBlockRefKeywords := getBlockVirtualRefKeywords(tree.Root)
  721. subTree := &parse.Tree{ID: rootID, Root: &ast.Node{Type: ast.NodeDocument}, Marks: tree.Marks}
  722. var keywords []string
  723. if "" != query && (0 == queryMethod || 1 == queryMethod) { // 只有关键字搜索和查询语法搜索才支持高亮
  724. if 0 == queryMethod {
  725. query = stringQuery(query)
  726. }
  727. typeFilter := buildTypeFilter(queryTypes)
  728. keywords = highlightByQuery(query, typeFilter, rootID)
  729. }
  730. for _, n := range nodes {
  731. var unlinks []*ast.Node
  732. ast.Walk(n, func(n *ast.Node, entering bool) ast.WalkStatus {
  733. if !entering {
  734. return ast.WalkContinue
  735. }
  736. if "1" == n.IALAttr("heading-fold") {
  737. // 折叠标题下被引用的块无法悬浮查看
  738. // The referenced block under the folded heading cannot be hovered to view https://github.com/siyuan-note/siyuan/issues/9582
  739. if (0 != mode && id != n.ID) || isDoc {
  740. unlinks = append(unlinks, n)
  741. return ast.WalkContinue
  742. }
  743. }
  744. if avs := n.IALAttr(av.NodeAttrNameAvs); "" != avs {
  745. // 填充属性视图角标 Display the database title on the block superscript https://github.com/siyuan-note/siyuan/issues/10545
  746. avNames := getAvNames(n.IALAttr(av.NodeAttrNameAvs))
  747. if "" != avNames {
  748. n.SetIALAttr(av.NodeAttrViewNames, avNames)
  749. }
  750. }
  751. if "" != n.ID {
  752. // 填充块引计数
  753. if cnt := refCount[n.ID]; 0 < cnt {
  754. n.SetIALAttr("refcount", strconv.Itoa(cnt))
  755. }
  756. }
  757. if 0 < len(keywords) {
  758. hitBlock := false
  759. for p := n.Parent; nil != p; p = p.Parent {
  760. if p.ID == id {
  761. hitBlock = true
  762. break
  763. }
  764. }
  765. if hitBlock {
  766. if ast.NodeCodeBlockCode == n.Type && !treenode.IsChartCodeBlockCode(n) {
  767. // 支持代码块搜索定位 https://github.com/siyuan-note/siyuan/issues/5520
  768. code := string(n.Tokens)
  769. markedCode := search.EncloseHighlighting(code, keywords, search.SearchMarkLeft, search.SearchMarkRight, Conf.Search.CaseSensitive, false)
  770. if code != markedCode {
  771. n.Tokens = gulu.Str.ToBytes(markedCode)
  772. return ast.WalkContinue
  773. }
  774. } else if markReplaceSpan(n, &unlinks, keywords, search.MarkDataType, luteEngine) {
  775. return ast.WalkContinue
  776. }
  777. }
  778. }
  779. if processVirtualRef(n, &unlinks, virtualBlockRefKeywords, refCount, luteEngine) {
  780. return ast.WalkContinue
  781. }
  782. return ast.WalkContinue
  783. })
  784. for _, unlink := range unlinks {
  785. unlink.Unlink()
  786. }
  787. subTree.Root.AppendChild(n)
  788. }
  789. luteEngine.RenderOptions.NodeIndexStart = index
  790. dom = luteEngine.Tree2BlockDOM(subTree, luteEngine.RenderOptions)
  791. SetRecentDocByTree(tree)
  792. return
  793. }
  794. func loadNodesByStartEnd(tree *parse.Tree, startID, endID string) (nodes []*ast.Node, eof bool) {
  795. node := treenode.GetNodeInTree(tree, startID)
  796. if nil == node {
  797. return
  798. }
  799. nodes = append(nodes, node)
  800. for n := node.Next; nil != n; n = n.Next {
  801. if treenode.IsInFoldedHeading(n, nil) {
  802. continue
  803. }
  804. nodes = append(nodes, n)
  805. if n.ID == endID {
  806. if next := n.Next; nil == next {
  807. eof = true
  808. } else {
  809. eof = util2.IsDocIAL(n.Tokens) || util2.IsDocIAL(next.Tokens)
  810. }
  811. break
  812. }
  813. }
  814. return
  815. }
  816. func loadNodesByMode(node *ast.Node, inputIndex, mode, size int, isDoc, isHeading bool) (nodes []*ast.Node, eof bool) {
  817. if 2 == mode /* 向下 */ {
  818. next := node.Next
  819. if ast.NodeHeading == node.Type && "1" == node.IALAttr("fold") {
  820. // 标题展开时进行动态加载导致重复内容 https://github.com/siyuan-note/siyuan/issues/4671
  821. // 这里要考虑折叠标题是最后一个块的情况
  822. if children := treenode.HeadingChildren(node); 0 < len(children) {
  823. next = children[len(children)-1].Next
  824. }
  825. }
  826. if nil == next {
  827. eof = true
  828. } else {
  829. eof = util2.IsDocIAL(node.Tokens) || util2.IsDocIAL(next.Tokens)
  830. }
  831. }
  832. count := 0
  833. switch mode {
  834. case 0: // 仅加载当前 ID
  835. nodes = append(nodes, node)
  836. if isDoc {
  837. for n := node.Next; nil != n; n = n.Next {
  838. if treenode.IsInFoldedHeading(n, nil) {
  839. continue
  840. }
  841. nodes = append(nodes, n)
  842. if 1 > count {
  843. count++
  844. } else {
  845. count += treenode.CountBlockNodes(n)
  846. }
  847. if size < count {
  848. break
  849. }
  850. }
  851. } else if isHeading {
  852. level := node.HeadingLevel
  853. for n := node.Next; nil != n; n = n.Next {
  854. if treenode.IsInFoldedHeading(n, node) {
  855. // 大纲点击折叠标题跳转聚焦 https://github.com/siyuan-note/siyuan/issues/4920
  856. // 多级标题折叠后上级块引浮窗中未折叠 https://github.com/siyuan-note/siyuan/issues/4997
  857. continue
  858. }
  859. if ast.NodeHeading == n.Type {
  860. if n.HeadingLevel <= level {
  861. break
  862. }
  863. }
  864. nodes = append(nodes, n)
  865. count++
  866. if size < count {
  867. break
  868. }
  869. }
  870. }
  871. case 4: // Ctrl+End 跳转到末尾后向上加载
  872. for n := node; nil != n; n = n.Previous {
  873. if treenode.IsInFoldedHeading(n, nil) {
  874. continue
  875. }
  876. nodes = append([]*ast.Node{n}, nodes...)
  877. if 1 > count {
  878. count++
  879. } else {
  880. count += treenode.CountBlockNodes(n)
  881. }
  882. if size < count {
  883. break
  884. }
  885. }
  886. eof = true
  887. case 1: // 向上加载
  888. for n := node.Previous; /* 从上一个节点开始加载 */ nil != n; n = n.Previous {
  889. if treenode.IsInFoldedHeading(n, nil) {
  890. continue
  891. }
  892. nodes = append([]*ast.Node{n}, nodes...)
  893. if 1 > count {
  894. count++
  895. } else {
  896. count += treenode.CountBlockNodes(n)
  897. }
  898. if size < count {
  899. break
  900. }
  901. }
  902. eof = nil == node.Previous
  903. case 2: // 向下加载
  904. for n := node.Next; /* 从下一个节点开始加载 */ nil != n; n = n.Next {
  905. if treenode.IsInFoldedHeading(n, node) {
  906. continue
  907. }
  908. nodes = append(nodes, n)
  909. if 1 > count {
  910. count++
  911. } else {
  912. count += treenode.CountBlockNodes(n)
  913. }
  914. if size < count {
  915. break
  916. }
  917. }
  918. case 3: // 上下都加载
  919. for n := node; nil != n; n = n.Previous {
  920. if treenode.IsInFoldedHeading(n, nil) {
  921. continue
  922. }
  923. nodes = append([]*ast.Node{n}, nodes...)
  924. if 1 > count {
  925. count++
  926. } else {
  927. count += treenode.CountBlockNodes(n)
  928. }
  929. if 0 < inputIndex {
  930. if 1 < count {
  931. break // 滑块指示器加载
  932. }
  933. } else {
  934. if size < count {
  935. break
  936. }
  937. }
  938. }
  939. if size/2 < count {
  940. size = size / 2
  941. } else {
  942. size = size - count
  943. }
  944. count = 0
  945. for n := node.Next; nil != n; n = n.Next {
  946. if treenode.IsInFoldedHeading(n, nil) {
  947. continue
  948. }
  949. nodes = append(nodes, n)
  950. if 1 > count {
  951. count++
  952. } else {
  953. count += treenode.CountBlockNodes(n)
  954. }
  955. if 0 < inputIndex {
  956. if size < count {
  957. break
  958. }
  959. } else {
  960. if size < count {
  961. break
  962. }
  963. }
  964. }
  965. }
  966. return
  967. }
  968. func writeTreeUpsertQueue(tree *parse.Tree) (err error) {
  969. if err = filesys.WriteTree(tree); nil != err {
  970. return
  971. }
  972. sql.UpsertTreeQueue(tree)
  973. return
  974. }
  975. func writeTreeIndexQueue(tree *parse.Tree) (err error) {
  976. if err = filesys.WriteTree(tree); nil != err {
  977. return
  978. }
  979. sql.IndexTreeQueue(tree)
  980. return
  981. }
  982. func indexWriteTreeIndexQueue(tree *parse.Tree) (err error) {
  983. treenode.IndexBlockTree(tree)
  984. return writeTreeIndexQueue(tree)
  985. }
  986. func indexWriteTreeUpsertQueue(tree *parse.Tree) (err error) {
  987. treenode.IndexBlockTree(tree)
  988. return writeTreeUpsertQueue(tree)
  989. }
  990. func renameWriteJSONQueue(tree *parse.Tree) (err error) {
  991. if err = filesys.WriteTree(tree); nil != err {
  992. return
  993. }
  994. sql.RenameTreeQueue(tree)
  995. treenode.IndexBlockTree(tree)
  996. return
  997. }
  998. func DuplicateDoc(tree *parse.Tree) {
  999. msgId := util.PushMsg(Conf.Language(116), 30000)
  1000. defer util.PushClearMsg(msgId)
  1001. resetTree(tree, "Duplicated")
  1002. createTreeTx(tree)
  1003. WaitForWritingFiles()
  1004. return
  1005. }
  1006. func createTreeTx(tree *parse.Tree) {
  1007. transaction := &Transaction{DoOperations: []*Operation{{Action: "create", Data: tree}}}
  1008. PerformTransactions(&[]*Transaction{transaction})
  1009. }
  1010. var createDocLock = sync.Mutex{}
  1011. func CreateDocByMd(boxID, p, title, md string, sorts []string) (tree *parse.Tree, err error) {
  1012. createDocLock.Lock()
  1013. defer createDocLock.Unlock()
  1014. box := Conf.Box(boxID)
  1015. if nil == box {
  1016. err = errors.New(Conf.Language(0))
  1017. return
  1018. }
  1019. luteEngine := util.NewLute()
  1020. dom := luteEngine.Md2BlockDOM(md, false)
  1021. tree, err = createDoc(box.ID, p, title, dom)
  1022. if nil != err {
  1023. return
  1024. }
  1025. WaitForWritingFiles()
  1026. ChangeFileTreeSort(box.ID, sorts)
  1027. return
  1028. }
  1029. func CreateWithMarkdown(boxID, hPath, md, parentID, id string, withMath bool) (retID string, err error) {
  1030. createDocLock.Lock()
  1031. defer createDocLock.Unlock()
  1032. box := Conf.Box(boxID)
  1033. if nil == box {
  1034. err = errors.New(Conf.Language(0))
  1035. return
  1036. }
  1037. WaitForWritingFiles()
  1038. luteEngine := util.NewLute()
  1039. if withMath {
  1040. luteEngine.SetInlineMath(true)
  1041. }
  1042. dom := luteEngine.Md2BlockDOM(md, false)
  1043. retID, err = createDocsByHPath(box.ID, hPath, dom, parentID, id)
  1044. WaitForWritingFiles()
  1045. return
  1046. }
  1047. func CreateDailyNote(boxID string) (p string, existed bool, err error) {
  1048. createDocLock.Lock()
  1049. defer createDocLock.Unlock()
  1050. box := Conf.Box(boxID)
  1051. if nil == box {
  1052. err = ErrBoxNotFound
  1053. return
  1054. }
  1055. boxConf := box.GetConf()
  1056. if "" == boxConf.DailyNoteSavePath || "/" == boxConf.DailyNoteSavePath {
  1057. err = errors.New(Conf.Language(49))
  1058. return
  1059. }
  1060. hPath, err := RenderGoTemplate(boxConf.DailyNoteSavePath)
  1061. if nil != err {
  1062. return
  1063. }
  1064. WaitForWritingFiles()
  1065. existRoot := treenode.GetBlockTreeRootByHPath(box.ID, hPath)
  1066. if nil != existRoot {
  1067. existed = true
  1068. p = existRoot.Path
  1069. tree, loadErr := LoadTreeByBlockID(existRoot.RootID)
  1070. if nil != loadErr {
  1071. logging.LogWarnf("load tree by block id [%s] failed: %v", existRoot.RootID, loadErr)
  1072. return
  1073. }
  1074. p = tree.Path
  1075. date := time.Now().Format("20060102")
  1076. if tree.Root.IALAttr("custom-dailynote-"+date) == "" {
  1077. tree.Root.SetIALAttr("custom-dailynote-"+date, date)
  1078. if err = indexWriteTreeUpsertQueue(tree); nil != err {
  1079. return
  1080. }
  1081. }
  1082. return
  1083. }
  1084. id, err := createDocsByHPath(box.ID, hPath, "", "", "")
  1085. if nil != err {
  1086. return
  1087. }
  1088. var templateTree *parse.Tree
  1089. var templateDom string
  1090. if "" != boxConf.DailyNoteTemplatePath {
  1091. tplPath := filepath.Join(util.DataDir, "templates", boxConf.DailyNoteTemplatePath)
  1092. if !filelock.IsExist(tplPath) {
  1093. logging.LogWarnf("not found daily note template [%s]", tplPath)
  1094. } else {
  1095. var renderErr error
  1096. templateTree, templateDom, renderErr = RenderTemplate(tplPath, id, false)
  1097. if nil != renderErr {
  1098. logging.LogWarnf("render daily note template [%s] failed: %s", boxConf.DailyNoteTemplatePath, err)
  1099. }
  1100. }
  1101. }
  1102. if "" != templateDom {
  1103. var tree *parse.Tree
  1104. tree, err = LoadTreeByBlockID(id)
  1105. if nil == err {
  1106. tree.Root.FirstChild.Unlink()
  1107. luteEngine := util.NewLute()
  1108. newTree := luteEngine.BlockDOM2Tree(templateDom)
  1109. var children []*ast.Node
  1110. for c := newTree.Root.FirstChild; nil != c; c = c.Next {
  1111. children = append(children, c)
  1112. }
  1113. for _, c := range children {
  1114. tree.Root.AppendChild(c)
  1115. }
  1116. // Creating a dailynote template supports doc attributes https://github.com/siyuan-note/siyuan/issues/10698
  1117. templateIALs := parse.IAL2Map(templateTree.Root.KramdownIAL)
  1118. for k, v := range templateIALs {
  1119. if "name" == k || "alias" == k || "bookmark" == k || "memo" == k || strings.HasPrefix(k, "custom-") {
  1120. tree.Root.SetIALAttr(k, v)
  1121. }
  1122. }
  1123. tree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
  1124. if err = indexWriteTreeUpsertQueue(tree); nil != err {
  1125. return
  1126. }
  1127. }
  1128. }
  1129. IncSync()
  1130. WaitForWritingFiles()
  1131. tree, err := LoadTreeByBlockID(id)
  1132. if nil != err {
  1133. logging.LogErrorf("load tree by block id [%s] failed: %v", id, err)
  1134. return
  1135. }
  1136. p = tree.Path
  1137. date := time.Now().Format("20060102")
  1138. tree.Root.SetIALAttr("custom-dailynote-"+date, date)
  1139. if err = indexWriteTreeUpsertQueue(tree); nil != err {
  1140. return
  1141. }
  1142. return
  1143. }
  1144. func GetHPathByPath(boxID, p string) (hPath string, err error) {
  1145. if "/" == p {
  1146. hPath = "/"
  1147. return
  1148. }
  1149. luteEngine := util.NewLute()
  1150. tree, err := filesys.LoadTree(boxID, p, luteEngine)
  1151. if nil != err {
  1152. return
  1153. }
  1154. hPath = tree.HPath
  1155. return
  1156. }
  1157. func GetHPathsByPaths(paths []string) (hPaths []string, err error) {
  1158. pathsBoxes := getBoxesByPaths(paths)
  1159. for p, box := range pathsBoxes {
  1160. if nil == box {
  1161. logging.LogWarnf("box not found by path [%s]", p)
  1162. continue
  1163. }
  1164. bt := treenode.GetBlockTreeByPath(p)
  1165. if nil == bt {
  1166. logging.LogWarnf("block tree not found by path [%s]", p)
  1167. continue
  1168. }
  1169. hpath := html.UnescapeString(bt.HPath)
  1170. hPaths = append(hPaths, box.Name+hpath)
  1171. }
  1172. return
  1173. }
  1174. func GetHPathByID(id string) (hPath string, err error) {
  1175. tree, err := LoadTreeByBlockID(id)
  1176. if nil != err {
  1177. return
  1178. }
  1179. hPath = tree.HPath
  1180. return
  1181. }
  1182. func GetFullHPathByID(id string) (hPath string, err error) {
  1183. tree, err := LoadTreeByBlockID(id)
  1184. if nil != err {
  1185. return
  1186. }
  1187. box := Conf.Box(tree.Box)
  1188. var boxName string
  1189. if nil != box {
  1190. boxName = box.Name
  1191. }
  1192. hPath = boxName + tree.HPath
  1193. return
  1194. }
  1195. func GetIDsByHPath(hpath, boxID string) (ret []string, err error) {
  1196. ret = []string{}
  1197. roots := treenode.GetBlockTreeRootsByHPath(boxID, hpath)
  1198. if 1 > len(roots) {
  1199. return
  1200. }
  1201. for _, root := range roots {
  1202. ret = append(ret, root.ID)
  1203. }
  1204. ret = gulu.Str.RemoveDuplicatedElem(ret)
  1205. if 1 > len(ret) {
  1206. ret = []string{}
  1207. }
  1208. return
  1209. }
  1210. func MoveDocs(fromPaths []string, toBoxID, toPath string, callback interface{}) (err error) {
  1211. toBox := Conf.Box(toBoxID)
  1212. if nil == toBox {
  1213. err = errors.New(Conf.Language(0))
  1214. return
  1215. }
  1216. fromPaths = util.FilterMoveDocFromPaths(fromPaths, toPath)
  1217. if 1 > len(fromPaths) {
  1218. return
  1219. }
  1220. pathsBoxes := getBoxesByPaths(fromPaths)
  1221. if 1 == len(fromPaths) {
  1222. // 移动到自己的父文档下的情况相当于不移动,直接返回
  1223. if fromBox := pathsBoxes[fromPaths[0]]; nil != fromBox && fromBox.ID == toBoxID {
  1224. parentDir := path.Dir(fromPaths[0])
  1225. if ("/" == toPath && "/" == parentDir) || (parentDir+".sy" == toPath) {
  1226. return
  1227. }
  1228. }
  1229. }
  1230. // 检查路径深度是否超过限制
  1231. for fromPath, fromBox := range pathsBoxes {
  1232. childDepth := util.GetChildDocDepth(filepath.Join(util.DataDir, fromBox.ID, fromPath))
  1233. if depth := strings.Count(toPath, "/") + childDepth; 6 < depth && !Conf.FileTree.AllowCreateDeeper {
  1234. err = errors.New(Conf.Language(118))
  1235. return
  1236. }
  1237. }
  1238. // A progress layer appears when moving more than 64 documents at once https://github.com/siyuan-note/siyuan/issues/9356
  1239. subDocsCount := 0
  1240. for fromPath, fromBox := range pathsBoxes {
  1241. subDocsCount += countSubDocs(fromBox.ID, fromPath)
  1242. }
  1243. needShowProgress := 64 < subDocsCount
  1244. if needShowProgress {
  1245. defer util.PushClearProgress()
  1246. }
  1247. WaitForWritingFiles()
  1248. luteEngine := util.NewLute()
  1249. count := 0
  1250. for fromPath, fromBox := range pathsBoxes {
  1251. count++
  1252. if needShowProgress {
  1253. util.PushEndlessProgress(fmt.Sprintf(Conf.Language(70), fmt.Sprintf("%d/%d", count, len(fromPaths))))
  1254. }
  1255. _, err = moveDoc(fromBox, fromPath, toBox, toPath, luteEngine, callback)
  1256. if nil != err {
  1257. return
  1258. }
  1259. }
  1260. cache.ClearDocsIAL()
  1261. IncSync()
  1262. return
  1263. }
  1264. func countSubDocs(box, p string) (ret int) {
  1265. p = strings.TrimSuffix(p, ".sy")
  1266. _ = filepath.Walk(filepath.Join(util.DataDir, box, p), func(path string, info os.FileInfo, err error) error {
  1267. if nil != err {
  1268. return err
  1269. }
  1270. if info.IsDir() {
  1271. return nil
  1272. }
  1273. if strings.HasSuffix(path, ".sy") {
  1274. ret++
  1275. }
  1276. return nil
  1277. })
  1278. return
  1279. }
  1280. func moveDoc(fromBox *Box, fromPath string, toBox *Box, toPath string, luteEngine *lute.Lute, callback interface{}) (newPath string, err error) {
  1281. isSameBox := fromBox.ID == toBox.ID
  1282. if isSameBox {
  1283. if !fromBox.Exist(toPath) {
  1284. err = ErrBlockNotFound
  1285. return
  1286. }
  1287. } else {
  1288. if !toBox.Exist(toPath) {
  1289. err = ErrBlockNotFound
  1290. return
  1291. }
  1292. }
  1293. tree, err := filesys.LoadTree(fromBox.ID, fromPath, luteEngine)
  1294. if nil != err {
  1295. err = ErrBlockNotFound
  1296. return
  1297. }
  1298. moveToRoot := "/" == toPath
  1299. toBlockID := tree.ID
  1300. fromFolder := path.Join(path.Dir(fromPath), tree.ID)
  1301. toFolder := "/"
  1302. if !moveToRoot {
  1303. var toTree *parse.Tree
  1304. if isSameBox {
  1305. toTree, err = filesys.LoadTree(fromBox.ID, toPath, luteEngine)
  1306. } else {
  1307. toTree, err = filesys.LoadTree(toBox.ID, toPath, luteEngine)
  1308. }
  1309. if nil != err {
  1310. err = ErrBlockNotFound
  1311. return
  1312. }
  1313. toBlockID = toTree.ID
  1314. toFolder = path.Join(path.Dir(toPath), toBlockID)
  1315. }
  1316. if isSameBox {
  1317. if err = fromBox.MkdirAll(toFolder); nil != err {
  1318. return
  1319. }
  1320. } else {
  1321. if err = toBox.MkdirAll(toFolder); nil != err {
  1322. return
  1323. }
  1324. }
  1325. needMoveSubDocs := fromBox.Exist(fromFolder)
  1326. if needMoveSubDocs {
  1327. // 移动子文档文件夹
  1328. newFolder := path.Join(toFolder, tree.ID)
  1329. if isSameBox {
  1330. if err = fromBox.Move(fromFolder, newFolder); nil != err {
  1331. return
  1332. }
  1333. } else {
  1334. absFromPath := filepath.Join(util.DataDir, fromBox.ID, fromFolder)
  1335. absToPath := filepath.Join(util.DataDir, toBox.ID, newFolder)
  1336. if filelock.IsExist(absToPath) {
  1337. filelock.Remove(absToPath)
  1338. }
  1339. if err = filelock.Rename(absFromPath, absToPath); nil != err {
  1340. msg := fmt.Sprintf(Conf.Language(5), fromBox.Name, fromPath, err)
  1341. logging.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, fromBox.ID, err)
  1342. err = errors.New(msg)
  1343. return
  1344. }
  1345. }
  1346. }
  1347. newPath = path.Join(toFolder, tree.ID+".sy")
  1348. if isSameBox {
  1349. if err = fromBox.Move(fromPath, newPath); nil != err {
  1350. return
  1351. }
  1352. tree, err = filesys.LoadTree(fromBox.ID, newPath, luteEngine)
  1353. if nil != err {
  1354. return
  1355. }
  1356. moveTree(tree)
  1357. } else {
  1358. absFromPath := filepath.Join(util.DataDir, fromBox.ID, fromPath)
  1359. absToPath := filepath.Join(util.DataDir, toBox.ID, newPath)
  1360. if err = filelock.Rename(absFromPath, absToPath); nil != err {
  1361. msg := fmt.Sprintf(Conf.Language(5), fromBox.Name, fromPath, err)
  1362. logging.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, fromBox.ID, err)
  1363. err = errors.New(msg)
  1364. return
  1365. }
  1366. tree, err = filesys.LoadTree(toBox.ID, newPath, luteEngine)
  1367. if nil != err {
  1368. return
  1369. }
  1370. moveTree(tree)
  1371. moveSorts(tree.ID, fromBox.ID, toBox.ID)
  1372. }
  1373. if needMoveSubDocs {
  1374. // 将其所有子文档的移动事件推送到前端 https://github.com/siyuan-note/siyuan/issues/11661
  1375. subDocsFolder := path.Join(toFolder, tree.ID)
  1376. syFiles := listSyFiles(path.Join(toBox.ID, subDocsFolder))
  1377. for _, syFile := range syFiles {
  1378. relPath := strings.TrimPrefix(syFile, "/"+path.Join(toBox.ID, toFolder))
  1379. subFromPath := path.Join(path.Dir(fromPath), relPath)
  1380. subToPath := path.Join(toFolder, relPath)
  1381. evt := util.NewCmdResult("moveDoc", 0, util.PushModeBroadcast)
  1382. evt.Data = map[string]interface{}{
  1383. "fromNotebook": fromBox.ID,
  1384. "fromPath": subFromPath,
  1385. "toNotebook": toBox.ID,
  1386. "toPath": path.Dir(subToPath) + ".sy",
  1387. "newPath": subToPath,
  1388. }
  1389. evt.Callback = callback
  1390. util.PushEvent(evt)
  1391. }
  1392. }
  1393. evt := util.NewCmdResult("moveDoc", 0, util.PushModeBroadcast)
  1394. evt.Data = map[string]interface{}{
  1395. "fromNotebook": fromBox.ID,
  1396. "fromPath": fromPath,
  1397. "toNotebook": toBox.ID,
  1398. "toPath": toPath,
  1399. "newPath": newPath,
  1400. }
  1401. evt.Callback = callback
  1402. util.PushEvent(evt)
  1403. return
  1404. }
  1405. func RemoveDoc(boxID, p string) {
  1406. box := Conf.Box(boxID)
  1407. if nil == box {
  1408. return
  1409. }
  1410. WaitForWritingFiles()
  1411. luteEngine := util.NewLute()
  1412. removeDoc(box, p, luteEngine)
  1413. IncSync()
  1414. return
  1415. }
  1416. func RemoveDocs(paths []string) {
  1417. util.PushEndlessProgress(Conf.Language(116))
  1418. defer util.PushClearProgress()
  1419. paths = util.FilterSelfChildDocs(paths)
  1420. pathsBoxes := getBoxesByPaths(paths)
  1421. WaitForWritingFiles()
  1422. luteEngine := util.NewLute()
  1423. for p, box := range pathsBoxes {
  1424. removeDoc(box, p, luteEngine)
  1425. }
  1426. return
  1427. }
  1428. func removeDoc(box *Box, p string, luteEngine *lute.Lute) {
  1429. tree, _ := filesys.LoadTree(box.ID, p, luteEngine)
  1430. if nil == tree {
  1431. return
  1432. }
  1433. historyDir, err := GetHistoryDir(HistoryOpDelete)
  1434. if nil != err {
  1435. logging.LogErrorf("get history dir failed: %s", err)
  1436. return
  1437. }
  1438. historyPath := filepath.Join(historyDir, box.ID, p)
  1439. absPath := filepath.Join(util.DataDir, box.ID, p)
  1440. if err = filelock.Copy(absPath, historyPath); nil != err {
  1441. logging.LogErrorf("backup [path=%s] to history [%s] failed: %s", absPath, historyPath, err)
  1442. return
  1443. }
  1444. // 关联的属性视图也要复制到历史中 https://github.com/siyuan-note/siyuan/issues/9567
  1445. avNodes := tree.Root.ChildrenByType(ast.NodeAttributeView)
  1446. for _, avNode := range avNodes {
  1447. srcAvPath := filepath.Join(util.DataDir, "storage", "av", avNode.AttributeViewID+".json")
  1448. destAvPath := filepath.Join(historyDir, "storage", "av", avNode.AttributeViewID+".json")
  1449. if copyErr := filelock.Copy(srcAvPath, destAvPath); nil != copyErr {
  1450. logging.LogErrorf("copy av [%s] failed: %s", srcAvPath, copyErr)
  1451. }
  1452. }
  1453. copyDocAssetsToDataAssets(box.ID, p)
  1454. removeIDs := treenode.RootChildIDs(tree.ID)
  1455. dir := path.Dir(p)
  1456. childrenDir := path.Join(dir, tree.ID)
  1457. existChildren := box.Exist(childrenDir)
  1458. if existChildren {
  1459. absChildrenDir := filepath.Join(util.DataDir, tree.Box, childrenDir)
  1460. historyPath = filepath.Join(historyDir, tree.Box, childrenDir)
  1461. if err = filelock.Copy(absChildrenDir, historyPath); nil != err {
  1462. logging.LogErrorf("backup [path=%s] to history [%s] failed: %s", absChildrenDir, historyPath, err)
  1463. return
  1464. }
  1465. }
  1466. indexHistoryDir(filepath.Base(historyDir), util.NewLute())
  1467. allRemoveRootIDs := []string{tree.ID}
  1468. allRemoveRootIDs = append(allRemoveRootIDs, removeIDs...)
  1469. for _, rootID := range allRemoveRootIDs {
  1470. removeTree, _ := LoadTreeByBlockID(rootID)
  1471. if nil == removeTree {
  1472. continue
  1473. }
  1474. // 刷新文档关联的数据库 https://github.com/siyuan-note/siyuan/issues/11731
  1475. syncDelete2AttributeView(removeTree.Root)
  1476. // 解绑数据库关联
  1477. removeAvBlockRel(removeTree.Root)
  1478. }
  1479. if existChildren {
  1480. if err = box.Remove(childrenDir); nil != err {
  1481. logging.LogErrorf("remove children dir [%s%s] failed: %s", box.ID, childrenDir, err)
  1482. return
  1483. }
  1484. logging.LogInfof("removed children dir [%s%s]", box.ID, childrenDir)
  1485. }
  1486. if err = box.Remove(p); nil != err {
  1487. logging.LogErrorf("remove [%s%s] failed: %s", box.ID, p, err)
  1488. return
  1489. }
  1490. logging.LogInfof("removed doc [%s%s]", box.ID, p)
  1491. box.removeSort(removeIDs)
  1492. RemoveRecentDoc(removeIDs)
  1493. if "/" != dir {
  1494. others, err := os.ReadDir(filepath.Join(util.DataDir, box.ID, dir))
  1495. if nil == err && 1 > len(others) {
  1496. box.Remove(dir)
  1497. }
  1498. }
  1499. evt := util.NewCmdResult("removeDoc", 0, util.PushModeBroadcast)
  1500. evt.Data = map[string]interface{}{
  1501. "ids": removeIDs,
  1502. }
  1503. util.PushEvent(evt)
  1504. task.AppendTask(task.DatabaseIndex, removeDoc0, box, p, childrenDir)
  1505. }
  1506. func removeDoc0(box *Box, p, childrenDir string) {
  1507. treenode.RemoveBlockTreesByPathPrefix(childrenDir)
  1508. sql.RemoveTreePathQueue(box.ID, childrenDir)
  1509. cache.RemoveDocIAL(p)
  1510. return
  1511. }
  1512. func RenameDoc(boxID, p, title string) (err error) {
  1513. box := Conf.Box(boxID)
  1514. if nil == box {
  1515. err = errors.New(Conf.Language(0))
  1516. return
  1517. }
  1518. WaitForWritingFiles()
  1519. luteEngine := util.NewLute()
  1520. tree, err := filesys.LoadTree(box.ID, p, luteEngine)
  1521. if nil != err {
  1522. return
  1523. }
  1524. title = removeInvisibleCharsInTitle(title)
  1525. if 512 < utf8.RuneCountInString(title) {
  1526. // 限制笔记本名和文档名最大长度为 `512` https://github.com/siyuan-note/siyuan/issues/6299
  1527. return errors.New(Conf.Language(106))
  1528. }
  1529. oldTitle := tree.Root.IALAttr("title")
  1530. if oldTitle == title {
  1531. return
  1532. }
  1533. if "" == title {
  1534. title = Conf.language(105)
  1535. }
  1536. title = strings.ReplaceAll(title, "/", "")
  1537. tree.HPath = path.Join(path.Dir(tree.HPath), title)
  1538. tree.Root.SetIALAttr("title", title)
  1539. tree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
  1540. if err = renameWriteJSONQueue(tree); nil != err {
  1541. return
  1542. }
  1543. refText := getNodeRefText(tree.Root)
  1544. evt := util.NewCmdResult("rename", 0, util.PushModeBroadcast)
  1545. evt.Data = map[string]interface{}{
  1546. "box": boxID,
  1547. "id": tree.Root.ID,
  1548. "path": p,
  1549. "title": title,
  1550. "refText": refText,
  1551. }
  1552. util.PushEvent(evt)
  1553. box.renameSubTrees(tree)
  1554. updateRefTextRenameDoc(tree)
  1555. IncSync()
  1556. return
  1557. }
  1558. func createDoc(boxID, p, title, dom string) (tree *parse.Tree, err error) {
  1559. title = removeInvisibleCharsInTitle(title)
  1560. if 512 < utf8.RuneCountInString(title) {
  1561. // 限制笔记本名和文档名最大长度为 `512` https://github.com/siyuan-note/siyuan/issues/6299
  1562. err = errors.New(Conf.Language(106))
  1563. return
  1564. }
  1565. title = strings.ReplaceAll(title, "/", "")
  1566. title = strings.TrimSpace(title)
  1567. if "" == title {
  1568. title = Conf.Language(105)
  1569. }
  1570. baseName := strings.TrimSpace(path.Base(p))
  1571. if "" == strings.TrimSuffix(baseName, ".sy") {
  1572. err = errors.New(Conf.Language(16))
  1573. return
  1574. }
  1575. if strings.HasPrefix(baseName, ".") {
  1576. err = errors.New(Conf.Language(13))
  1577. return
  1578. }
  1579. box := Conf.Box(boxID)
  1580. if nil == box {
  1581. err = errors.New(Conf.Language(0))
  1582. return
  1583. }
  1584. id := strings.TrimSuffix(path.Base(p), ".sy")
  1585. var hPath string
  1586. folder := path.Dir(p)
  1587. if "/" != folder {
  1588. parentID := path.Base(folder)
  1589. parentTree, loadErr := LoadTreeByBlockID(parentID)
  1590. if nil != loadErr {
  1591. logging.LogErrorf("get parent tree [%s] failed", parentID)
  1592. err = ErrBlockNotFound
  1593. return
  1594. }
  1595. hPath = path.Join(parentTree.HPath, title)
  1596. } else {
  1597. hPath = "/" + title
  1598. }
  1599. if depth := strings.Count(p, "/"); 7 < depth && !Conf.FileTree.AllowCreateDeeper {
  1600. err = errors.New(Conf.Language(118))
  1601. return
  1602. }
  1603. if !box.Exist(folder) {
  1604. if err = box.MkdirAll(folder); nil != err {
  1605. return
  1606. }
  1607. }
  1608. if box.Exist(p) {
  1609. err = errors.New(Conf.Language(1))
  1610. return
  1611. }
  1612. luteEngine := util.NewLute()
  1613. tree = luteEngine.BlockDOM2Tree(dom)
  1614. tree.Box = boxID
  1615. tree.Path = p
  1616. tree.HPath = hPath
  1617. tree.ID = id
  1618. tree.Root.ID = id
  1619. tree.Root.Spec = "1"
  1620. updated := util.TimeFromID(id)
  1621. tree.Root.KramdownIAL = [][]string{{"id", id}, {"title", html.EscapeAttrVal(title)}, {"updated", updated}}
  1622. if nil == tree.Root.FirstChild {
  1623. tree.Root.AppendChild(treenode.NewParagraph())
  1624. }
  1625. // 如果段落块中仅包含一个 mp3/mp4 超链接,则将其转换为音视频块
  1626. // Convert mp3 and mp4 hyperlinks to audio and video when moving cloud inbox to docs https://github.com/siyuan-note/siyuan/issues/9778
  1627. var unlinks []*ast.Node
  1628. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  1629. if !entering {
  1630. return ast.WalkContinue
  1631. }
  1632. if ast.NodeParagraph == n.Type {
  1633. link := n.FirstChild
  1634. if nil != link && link.IsTextMarkType("a") {
  1635. if strings.HasSuffix(link.TextMarkAHref, ".mp3") {
  1636. unlinks = append(unlinks, n)
  1637. audio := &ast.Node{ID: n.ID, Type: ast.NodeAudio, Tokens: []byte("<audio controls=\"controls\" src=\"" + link.TextMarkAHref + "\" data-src=\"" + link.TextMarkAHref + "\"></audio>")}
  1638. audio.SetIALAttr("id", n.ID)
  1639. audio.SetIALAttr("updated", util.TimeFromID(n.ID))
  1640. n.InsertBefore(audio)
  1641. } else if strings.HasSuffix(link.TextMarkAHref, ".mp4") {
  1642. unlinks = append(unlinks, n)
  1643. video := &ast.Node{ID: n.ID, Type: ast.NodeVideo, Tokens: []byte("<video controls=\"controls\" src=\"" + link.TextMarkAHref + "\" data-src=\"" + link.TextMarkAHref + "\"></video>")}
  1644. video.SetIALAttr("id", n.ID)
  1645. video.SetIALAttr("updated", util.TimeFromID(n.ID))
  1646. n.InsertBefore(video)
  1647. }
  1648. }
  1649. }
  1650. return ast.WalkContinue
  1651. })
  1652. for _, unlink := range unlinks {
  1653. unlink.Unlink()
  1654. }
  1655. transaction := &Transaction{DoOperations: []*Operation{{Action: "create", Data: tree}}}
  1656. PerformTransactions(&[]*Transaction{transaction})
  1657. WaitForWritingFiles()
  1658. return
  1659. }
  1660. func removeInvisibleCharsInTitle(title string) string {
  1661. // 不要踢掉 零宽连字符,否则有的 Emoji 会变形 https://github.com/siyuan-note/siyuan/issues/11480
  1662. title = strings.ReplaceAll(title, string(gulu.ZWJ), "__@ZWJ@__")
  1663. title = gulu.Str.RemoveInvisible(title)
  1664. title = strings.ReplaceAll(title, "__@ZWJ@__", string(gulu.ZWJ))
  1665. return title
  1666. }
  1667. func moveSorts(rootID, fromBox, toBox string) {
  1668. root := treenode.GetBlockTree(rootID)
  1669. if nil == root {
  1670. return
  1671. }
  1672. fromRootSorts := map[string]int{}
  1673. ids := treenode.RootChildIDs(rootID)
  1674. fromConfPath := filepath.Join(util.DataDir, fromBox, ".siyuan", "sort.json")
  1675. fromFullSortIDs := map[string]int{}
  1676. if filelock.IsExist(fromConfPath) {
  1677. data, err := filelock.ReadFile(fromConfPath)
  1678. if nil != err {
  1679. logging.LogErrorf("read sort conf failed: %s", err)
  1680. return
  1681. }
  1682. if err = gulu.JSON.UnmarshalJSON(data, &fromFullSortIDs); nil != err {
  1683. logging.LogErrorf("unmarshal sort conf failed: %s", err)
  1684. }
  1685. }
  1686. for _, id := range ids {
  1687. fromRootSorts[id] = fromFullSortIDs[id]
  1688. }
  1689. toConfPath := filepath.Join(util.DataDir, toBox, ".siyuan", "sort.json")
  1690. toFullSortIDs := map[string]int{}
  1691. if filelock.IsExist(toConfPath) {
  1692. data, err := filelock.ReadFile(toConfPath)
  1693. if nil != err {
  1694. logging.LogErrorf("read sort conf failed: %s", err)
  1695. return
  1696. }
  1697. if err = gulu.JSON.UnmarshalJSON(data, &toFullSortIDs); nil != err {
  1698. logging.LogErrorf("unmarshal sort conf failed: %s", err)
  1699. return
  1700. }
  1701. }
  1702. for id, sortVal := range fromRootSorts {
  1703. toFullSortIDs[id] = sortVal
  1704. }
  1705. data, err := gulu.JSON.MarshalJSON(toFullSortIDs)
  1706. if nil != err {
  1707. logging.LogErrorf("marshal sort conf failed: %s", err)
  1708. return
  1709. }
  1710. if err = filelock.WriteFile(toConfPath, data); nil != err {
  1711. logging.LogErrorf("write sort conf failed: %s", err)
  1712. return
  1713. }
  1714. }
  1715. func ChangeFileTreeSort(boxID string, paths []string) {
  1716. if 1 > len(paths) {
  1717. return
  1718. }
  1719. WaitForWritingFiles()
  1720. box := Conf.Box(boxID)
  1721. sortIDs := map[string]int{}
  1722. max := 0
  1723. for i, p := range paths {
  1724. id := strings.TrimSuffix(path.Base(p), ".sy")
  1725. sortIDs[id] = i + 1
  1726. if i == len(paths)-1 {
  1727. max = i + 2
  1728. }
  1729. }
  1730. p := paths[0]
  1731. parentPath := path.Dir(p)
  1732. absParentPath := filepath.Join(util.DataDir, boxID, parentPath)
  1733. files, err := os.ReadDir(absParentPath)
  1734. if nil != err {
  1735. logging.LogErrorf("read dir [%s] failed: %s", absParentPath, err)
  1736. }
  1737. sortFolderIDs := map[string]int{}
  1738. for _, f := range files {
  1739. if !strings.HasSuffix(f.Name(), ".sy") {
  1740. continue
  1741. }
  1742. id := strings.TrimSuffix(f.Name(), ".sy")
  1743. val := sortIDs[id]
  1744. if 0 == val {
  1745. val = max
  1746. max++
  1747. }
  1748. sortFolderIDs[id] = val
  1749. }
  1750. confDir := filepath.Join(util.DataDir, box.ID, ".siyuan")
  1751. if err = os.MkdirAll(confDir, 0755); nil != err {
  1752. logging.LogErrorf("create conf dir failed: %s", err)
  1753. return
  1754. }
  1755. confPath := filepath.Join(confDir, "sort.json")
  1756. fullSortIDs := map[string]int{}
  1757. var data []byte
  1758. if filelock.IsExist(confPath) {
  1759. data, err = filelock.ReadFile(confPath)
  1760. if nil != err {
  1761. logging.LogErrorf("read sort conf failed: %s", err)
  1762. return
  1763. }
  1764. if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); nil != err {
  1765. logging.LogErrorf("unmarshal sort conf failed: %s", err)
  1766. }
  1767. }
  1768. for sortID, sortVal := range sortFolderIDs {
  1769. fullSortIDs[sortID] = sortVal
  1770. }
  1771. data, err = gulu.JSON.MarshalJSON(fullSortIDs)
  1772. if nil != err {
  1773. logging.LogErrorf("marshal sort conf failed: %s", err)
  1774. return
  1775. }
  1776. if err = filelock.WriteFile(confPath, data); nil != err {
  1777. logging.LogErrorf("write sort conf failed: %s", err)
  1778. return
  1779. }
  1780. IncSync()
  1781. }
  1782. func (box *Box) fillSort(files *[]*File) {
  1783. confPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json")
  1784. if !filelock.IsExist(confPath) {
  1785. return
  1786. }
  1787. data, err := filelock.ReadFile(confPath)
  1788. if nil != err {
  1789. logging.LogErrorf("read sort conf failed: %s", err)
  1790. return
  1791. }
  1792. fullSortIDs := map[string]int{}
  1793. if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); nil != err {
  1794. logging.LogErrorf("unmarshal sort conf failed: %s", err)
  1795. return
  1796. }
  1797. for _, f := range *files {
  1798. id := strings.TrimSuffix(f.ID, ".sy")
  1799. f.Sort = fullSortIDs[id]
  1800. }
  1801. }
  1802. func (box *Box) removeSort(ids []string) {
  1803. confPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json")
  1804. if !filelock.IsExist(confPath) {
  1805. return
  1806. }
  1807. data, err := filelock.ReadFile(confPath)
  1808. if nil != err {
  1809. logging.LogErrorf("read sort conf failed: %s", err)
  1810. return
  1811. }
  1812. fullSortIDs := map[string]int{}
  1813. if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); nil != err {
  1814. logging.LogErrorf("unmarshal sort conf failed: %s", err)
  1815. return
  1816. }
  1817. for _, toRemove := range ids {
  1818. delete(fullSortIDs, toRemove)
  1819. }
  1820. data, err = gulu.JSON.MarshalJSON(fullSortIDs)
  1821. if nil != err {
  1822. logging.LogErrorf("marshal sort conf failed: %s", err)
  1823. return
  1824. }
  1825. if err = filelock.WriteFile(confPath, data); nil != err {
  1826. logging.LogErrorf("write sort conf failed: %s", err)
  1827. return
  1828. }
  1829. }
  1830. func (box *Box) addMinSort(parentPath, id string) {
  1831. docs, _, err := ListDocTree(box.ID, parentPath, util.SortModeUnassigned, false, false, 1)
  1832. if nil != err {
  1833. logging.LogErrorf("list doc tree failed: %s", err)
  1834. return
  1835. }
  1836. sortVal := 0
  1837. if 0 < len(docs) {
  1838. sortVal = docs[0].Sort - 1
  1839. }
  1840. confDir := filepath.Join(util.DataDir, box.ID, ".siyuan")
  1841. if err = os.MkdirAll(confDir, 0755); nil != err {
  1842. logging.LogErrorf("create conf dir failed: %s", err)
  1843. return
  1844. }
  1845. confPath := filepath.Join(confDir, "sort.json")
  1846. fullSortIDs := map[string]int{}
  1847. var data []byte
  1848. if filelock.IsExist(confPath) {
  1849. data, err = filelock.ReadFile(confPath)
  1850. if nil != err {
  1851. logging.LogErrorf("read sort conf failed: %s", err)
  1852. return
  1853. }
  1854. if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); nil != err {
  1855. logging.LogErrorf("unmarshal sort conf failed: %s", err)
  1856. }
  1857. }
  1858. fullSortIDs[id] = sortVal
  1859. data, err = gulu.JSON.MarshalJSON(fullSortIDs)
  1860. if nil != err {
  1861. logging.LogErrorf("marshal sort conf failed: %s", err)
  1862. return
  1863. }
  1864. if err = filelock.WriteFile(confPath, data); nil != err {
  1865. logging.LogErrorf("write sort conf failed: %s", err)
  1866. return
  1867. }
  1868. }