import.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  1. // SiYuan - Build Your Eternal Digital Garden
  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. "encoding/json"
  20. "errors"
  21. "fmt"
  22. "io"
  23. "io/fs"
  24. "math/rand"
  25. "os"
  26. "path"
  27. "path/filepath"
  28. "runtime/debug"
  29. "sort"
  30. "strconv"
  31. "strings"
  32. "time"
  33. "github.com/88250/gulu"
  34. "github.com/88250/lute/ast"
  35. "github.com/88250/lute/html"
  36. "github.com/88250/lute/html/atom"
  37. "github.com/88250/lute/parse"
  38. "github.com/88250/lute/render"
  39. "github.com/siyuan-note/filelock"
  40. "github.com/siyuan-note/logging"
  41. "github.com/siyuan-note/siyuan/kernel/filesys"
  42. "github.com/siyuan-note/siyuan/kernel/sql"
  43. "github.com/siyuan-note/siyuan/kernel/treenode"
  44. "github.com/siyuan-note/siyuan/kernel/util"
  45. )
  46. func ImportSY(zipPath, boxID, toPath string) (err error) {
  47. util.PushEndlessProgress(Conf.Language(73))
  48. defer util.ClearPushProgress(100)
  49. baseName := filepath.Base(zipPath)
  50. ext := filepath.Ext(baseName)
  51. baseName = strings.TrimSuffix(baseName, ext)
  52. unzipPath := filepath.Join(filepath.Dir(zipPath), baseName+"-"+gulu.Rand.String(7))
  53. err = gulu.Zip.Unzip(zipPath, unzipPath)
  54. if nil != err {
  55. return
  56. }
  57. defer os.RemoveAll(unzipPath)
  58. var syPaths []string
  59. filepath.Walk(unzipPath, func(path string, info fs.FileInfo, err error) error {
  60. if nil != err {
  61. return err
  62. }
  63. if !info.IsDir() && strings.HasSuffix(info.Name(), ".sy") {
  64. syPaths = append(syPaths, path)
  65. }
  66. return nil
  67. })
  68. unzipRootPaths, err := filepath.Glob(unzipPath + "/*")
  69. if nil != err {
  70. return
  71. }
  72. if 1 != len(unzipRootPaths) {
  73. logging.LogErrorf("invalid .sy.zip")
  74. return errors.New("invalid .sy.zip")
  75. }
  76. unzipRootPath := unzipRootPaths[0]
  77. luteEngine := util.NewLute()
  78. blockIDs := map[string]string{}
  79. trees := map[string]*parse.Tree{}
  80. // 重新生成块 ID
  81. for _, syPath := range syPaths {
  82. data, readErr := os.ReadFile(syPath)
  83. if nil != readErr {
  84. logging.LogErrorf("read .sy [%s] failed: %s", syPath, readErr)
  85. err = readErr
  86. return
  87. }
  88. tree, _, parseErr := parse.ParseJSON(data, luteEngine.ParseOptions)
  89. if nil != parseErr {
  90. logging.LogErrorf("parse .sy [%s] failed: %s", syPath, parseErr)
  91. err = parseErr
  92. return
  93. }
  94. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  95. if !entering {
  96. return ast.WalkContinue
  97. }
  98. if "" != n.ID {
  99. newNodeID := ast.NewNodeID()
  100. blockIDs[n.ID] = newNodeID
  101. n.ID = newNodeID
  102. n.SetIALAttr("id", newNodeID)
  103. }
  104. return ast.WalkContinue
  105. })
  106. tree.ID = tree.Root.ID
  107. tree.Path = filepath.ToSlash(strings.TrimPrefix(syPath, unzipRootPath))
  108. trees[tree.ID] = tree
  109. }
  110. // 引用和嵌入指向重新生成的块 ID
  111. for _, tree := range trees {
  112. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  113. if !entering {
  114. return ast.WalkContinue
  115. }
  116. if treenode.IsBlockRef(n) {
  117. defID, _, _ := treenode.GetBlockRef(n)
  118. newDefID := blockIDs[defID]
  119. if "" != newDefID {
  120. if ast.NodeBlockRef == n.Type {
  121. if id := n.ChildByType(ast.NodeBlockRefID); nil != id {
  122. id.Tokens = []byte(newDefID)
  123. }
  124. } else {
  125. n.TextMarkBlockRefID = newDefID
  126. }
  127. }
  128. } else if ast.NodeBlockQueryEmbedScript == n.Type {
  129. for oldID, newID := range blockIDs {
  130. // 导入 `.sy.zip` 后查询嵌入块失效 https://github.com/siyuan-note/siyuan/issues/5316
  131. n.Tokens = bytes.ReplaceAll(n.Tokens, []byte(oldID), []byte(newID))
  132. }
  133. }
  134. return ast.WalkContinue
  135. })
  136. }
  137. // 写回 .sy
  138. for _, tree := range trees {
  139. syPath := filepath.Join(unzipRootPath, tree.Path)
  140. if "" == tree.Root.Spec {
  141. luteEngine.NestedInlines2FlattedSpans(tree)
  142. tree.Root.Spec = "1"
  143. }
  144. renderer := render.NewJSONRenderer(tree, luteEngine.RenderOptions)
  145. data := renderer.Render()
  146. buf := bytes.Buffer{}
  147. buf.Grow(4096)
  148. if err = json.Indent(&buf, data, "", "\t"); nil != err {
  149. return
  150. }
  151. data = buf.Bytes()
  152. if err = os.WriteFile(syPath, data, 0644); nil != err {
  153. logging.LogErrorf("write .sy [%s] failed: %s", syPath, err)
  154. return
  155. }
  156. newSyPath := filepath.Join(filepath.Dir(syPath), tree.ID+".sy")
  157. if err = os.Rename(syPath, newSyPath); nil != err {
  158. logging.LogErrorf("rename .sy from [%s] to [%s] failed: %s", syPath, newSyPath, err)
  159. return
  160. }
  161. }
  162. // 合并 sort.json
  163. fullSortIDs := map[string]int{}
  164. sortIDs := map[string]int{}
  165. var sortData []byte
  166. var sortErr error
  167. sortPath := filepath.Join(unzipRootPath, ".siyuan", "sort.json")
  168. if gulu.File.IsExist(sortPath) {
  169. sortData, sortErr = filelock.NoLockFileRead(sortPath)
  170. if nil != sortErr {
  171. logging.LogErrorf("read import sort conf failed: %s", sortErr)
  172. }
  173. if sortErr = gulu.JSON.UnmarshalJSON(sortData, &sortIDs); nil != sortErr {
  174. logging.LogErrorf("unmarshal sort conf failed: %s", sortErr)
  175. }
  176. sortPath = filepath.Join(util.DataDir, boxID, ".siyuan", "sort.json")
  177. if gulu.File.IsExist(sortPath) {
  178. sortData, sortErr = filelock.NoLockFileRead(sortPath)
  179. if nil != sortErr {
  180. logging.LogErrorf("read box sort conf failed: %s", sortErr)
  181. }
  182. if sortErr = gulu.JSON.UnmarshalJSON(sortData, &fullSortIDs); nil != sortErr {
  183. logging.LogErrorf("unmarshal box sort conf failed: %s", sortErr)
  184. }
  185. }
  186. for oldID, sort := range sortIDs {
  187. if newID := blockIDs[oldID]; "" != newID {
  188. fullSortIDs[newID] = sort
  189. }
  190. }
  191. sortData, sortErr = gulu.JSON.MarshalJSON(fullSortIDs)
  192. if nil != sortErr {
  193. logging.LogErrorf("marshal box sort conf failed: %s", sortErr)
  194. } else {
  195. sortErr = filelock.NoLockFileWrite(sortPath, sortData)
  196. if nil != sortErr {
  197. logging.LogErrorf("write box sort conf failed: %s", sortErr)
  198. }
  199. }
  200. }
  201. // 重命名文件路径
  202. renamePaths := map[string]string{}
  203. filepath.Walk(unzipRootPath, func(path string, info fs.FileInfo, err error) error {
  204. if nil != err {
  205. return err
  206. }
  207. if info.IsDir() && util.IsIDPattern(info.Name()) {
  208. renamePaths[path] = path
  209. }
  210. return nil
  211. })
  212. for p, _ := range renamePaths {
  213. originalPath := p
  214. p = strings.TrimPrefix(p, unzipRootPath)
  215. p = filepath.ToSlash(p)
  216. parts := strings.Split(p, "/")
  217. buf := bytes.Buffer{}
  218. buf.WriteString("/")
  219. for i, part := range parts {
  220. if "" == part {
  221. continue
  222. }
  223. newNodeID := blockIDs[part]
  224. if "" != newNodeID {
  225. buf.WriteString(newNodeID)
  226. } else {
  227. buf.WriteString(part)
  228. }
  229. if i < len(parts)-1 {
  230. buf.WriteString("/")
  231. }
  232. }
  233. newPath := buf.String()
  234. renamePaths[originalPath] = filepath.Join(unzipRootPath, newPath)
  235. }
  236. var oldPaths []string
  237. for oldPath, _ := range renamePaths {
  238. oldPaths = append(oldPaths, oldPath)
  239. }
  240. sort.Slice(oldPaths, func(i, j int) bool {
  241. return strings.Count(oldPaths[i], string(os.PathSeparator)) < strings.Count(oldPaths[j], string(os.PathSeparator))
  242. })
  243. for i, oldPath := range oldPaths {
  244. newPath := renamePaths[oldPath]
  245. if err = os.Rename(oldPath, newPath); nil != err {
  246. logging.LogErrorf("rename path from [%s] to [%s] failed: %s", oldPath, renamePaths[oldPath], err)
  247. return errors.New("rename path failed")
  248. }
  249. delete(renamePaths, oldPath)
  250. var toRemoves []string
  251. newRenamedPaths := map[string]string{}
  252. for oldP, newP := range renamePaths {
  253. if strings.HasPrefix(oldP, oldPath) {
  254. renamedOldP := strings.Replace(oldP, oldPath, newPath, 1)
  255. newRenamedPaths[renamedOldP] = newP
  256. toRemoves = append(toRemoves, oldPath)
  257. }
  258. }
  259. for _, toRemove := range toRemoves {
  260. delete(renamePaths, toRemove)
  261. }
  262. for oldP, newP := range newRenamedPaths {
  263. renamePaths[oldP] = newP
  264. }
  265. for j := i + 1; j < len(oldPaths); j++ {
  266. if strings.HasPrefix(oldPaths[j], oldPath) {
  267. renamedOldP := strings.Replace(oldPaths[j], oldPath, newPath, 1)
  268. oldPaths[j] = renamedOldP
  269. }
  270. }
  271. }
  272. var assetsDirs []string
  273. filepath.Walk(unzipRootPath, func(path string, info fs.FileInfo, err error) error {
  274. if strings.Contains(path, "assets") && info.IsDir() {
  275. assetsDirs = append(assetsDirs, path)
  276. }
  277. return nil
  278. })
  279. for _, assets := range assetsDirs {
  280. if gulu.File.IsDir(assets) {
  281. dataAssets := filepath.Join(util.DataDir, "assets")
  282. if err = filesys.Copy(assets, dataAssets); nil != err {
  283. logging.LogErrorf("copy assets from [%s] to [%s] failed: %s", assets, dataAssets, err)
  284. return
  285. }
  286. }
  287. os.RemoveAll(assets)
  288. }
  289. filesys.LockWriteFile()
  290. defer filesys.UnlockWriteFile()
  291. filelock.ReleaseAllFileLocks()
  292. var baseTargetPath string
  293. if "/" == toPath {
  294. baseTargetPath = "/"
  295. } else {
  296. block := treenode.GetBlockTreeRootByPath(boxID, toPath)
  297. if nil == block {
  298. logging.LogErrorf("not found block by path [%s]", toPath)
  299. return nil
  300. }
  301. baseTargetPath = strings.TrimSuffix(block.Path, ".sy")
  302. }
  303. targetDir := filepath.Join(util.DataDir, boxID, baseTargetPath)
  304. if err = os.MkdirAll(targetDir, 0755); nil != err {
  305. return
  306. }
  307. if err = stableCopy(unzipRootPath, targetDir); nil != err {
  308. logging.LogErrorf("copy data dir from [%s] to [%s] failed: %s", unzipRootPath, util.DataDir, err)
  309. err = errors.New("copy data failed")
  310. return
  311. }
  312. IncSync()
  313. FullReindex()
  314. return
  315. }
  316. func ImportData(zipPath string) (err error) {
  317. util.PushEndlessProgress(Conf.Language(73))
  318. defer util.ClearPushProgress(100)
  319. baseName := filepath.Base(zipPath)
  320. ext := filepath.Ext(baseName)
  321. baseName = strings.TrimSuffix(baseName, ext)
  322. unzipPath := filepath.Join(filepath.Dir(zipPath), baseName)
  323. err = gulu.Zip.Unzip(zipPath, unzipPath)
  324. if nil != err {
  325. return
  326. }
  327. defer os.RemoveAll(unzipPath)
  328. files, err := filepath.Glob(filepath.Join(unzipPath, "*/*.sy"))
  329. if nil != err {
  330. logging.LogErrorf("check data.zip failed: %s", err)
  331. return errors.New("check data.zip failed")
  332. }
  333. if 0 < len(files) {
  334. return errors.New("invalid data.zip")
  335. }
  336. dirs, err := os.ReadDir(unzipPath)
  337. if nil != err {
  338. logging.LogErrorf("check data.zip failed: %s", err)
  339. return errors.New("check data.zip failed")
  340. }
  341. if 1 != len(dirs) {
  342. return errors.New("invalid data.zip")
  343. }
  344. filesys.LockWriteFile()
  345. defer filesys.UnlockWriteFile()
  346. filelock.ReleaseAllFileLocks()
  347. tmpDataPath := filepath.Join(unzipPath, dirs[0].Name())
  348. if err = stableCopy(tmpDataPath, util.DataDir); nil != err {
  349. logging.LogErrorf("copy data dir from [%s] to [%s] failed: %s", tmpDataPath, util.DataDir, err)
  350. err = errors.New("copy data failed")
  351. return
  352. }
  353. IncSync()
  354. FullReindex()
  355. return
  356. }
  357. func ImportFromLocalPath(boxID, localPath string, toPath string) (err error) {
  358. util.PushEndlessProgress(Conf.Language(73))
  359. WaitForWritingFiles()
  360. var baseHPath, baseTargetPath, boxLocalPath string
  361. if "/" == toPath {
  362. baseHPath = "/"
  363. baseTargetPath = "/"
  364. } else {
  365. block := treenode.GetBlockTreeRootByPath(boxID, toPath)
  366. if nil == block {
  367. logging.LogErrorf("not found block by path [%s]", toPath)
  368. return nil
  369. }
  370. baseHPath = block.HPath
  371. baseTargetPath = strings.TrimSuffix(block.Path, ".sy")
  372. }
  373. boxLocalPath = filepath.Join(util.DataDir, boxID)
  374. luteEngine := NewLute()
  375. if gulu.File.IsDir(localPath) {
  376. // 收集所有资源文件
  377. assets := map[string]string{}
  378. filepath.Walk(localPath, func(currentPath string, info os.FileInfo, walkErr error) error {
  379. if localPath == currentPath {
  380. return nil
  381. }
  382. if strings.HasPrefix(info.Name(), ".") {
  383. if info.IsDir() {
  384. return filepath.SkipDir
  385. }
  386. return nil
  387. }
  388. if !strings.HasSuffix(info.Name(), ".md") && !strings.HasSuffix(info.Name(), ".markdown") {
  389. dest := currentPath
  390. assets[dest] = currentPath
  391. return nil
  392. }
  393. return nil
  394. })
  395. targetPaths := map[string]string{}
  396. assetsDone := map[string]string{}
  397. // md 转换 sy
  398. i := 0
  399. filepath.Walk(localPath, func(currentPath string, info os.FileInfo, walkErr error) error {
  400. if strings.HasPrefix(info.Name(), ".") {
  401. if info.IsDir() {
  402. return filepath.SkipDir
  403. }
  404. return nil
  405. }
  406. var tree *parse.Tree
  407. var ext string
  408. title := info.Name()
  409. if !info.IsDir() {
  410. ext = path.Ext(info.Name())
  411. title = strings.TrimSuffix(info.Name(), ext)
  412. }
  413. id := ast.NewNodeID()
  414. curRelPath := filepath.ToSlash(strings.TrimPrefix(currentPath, localPath))
  415. targetPath := path.Join(baseTargetPath, id)
  416. if "" == curRelPath {
  417. curRelPath = "/"
  418. } else {
  419. dirPath := targetPaths[path.Dir(curRelPath)]
  420. targetPath = path.Join(dirPath, id)
  421. }
  422. targetPath = strings.ReplaceAll(targetPath, ".sy/", "/")
  423. targetPath += ".sy"
  424. targetPaths[curRelPath] = targetPath
  425. hPath := path.Join(baseHPath, filepath.ToSlash(strings.TrimPrefix(currentPath, localPath)))
  426. hPath = strings.TrimSuffix(hPath, ext)
  427. if info.IsDir() {
  428. tree = treenode.NewTree(boxID, targetPath, hPath, title)
  429. if err = filesys.WriteTree(tree); nil != err {
  430. return io.EOF
  431. }
  432. return nil
  433. }
  434. if !strings.HasSuffix(info.Name(), ".md") && !strings.HasSuffix(info.Name(), ".markdown") {
  435. return nil
  436. }
  437. data, readErr := os.ReadFile(currentPath)
  438. if nil != readErr {
  439. err = readErr
  440. return io.EOF
  441. }
  442. tree = parseKTree(data)
  443. if nil == tree {
  444. logging.LogErrorf("parse tree [%s] failed", currentPath)
  445. return nil
  446. }
  447. imgHtmlBlock2InlineImg(tree)
  448. tree.ID = id
  449. tree.Root.ID = id
  450. tree.Root.SetIALAttr("id", tree.Root.ID)
  451. tree.Root.SetIALAttr("title", title)
  452. tree.Box = boxID
  453. targetPath = path.Join(path.Dir(targetPath), tree.Root.ID+".sy")
  454. tree.Path = targetPath
  455. targetPaths[curRelPath] = targetPath
  456. tree.HPath = hPath
  457. tree.Root.Spec = "1"
  458. luteEngine.NestedInlines2FlattedSpans(tree)
  459. docDirLocalPath := filepath.Dir(filepath.Join(boxLocalPath, targetPath))
  460. assetDirPath := getAssetsDir(boxLocalPath, docDirLocalPath)
  461. currentDir := filepath.Dir(currentPath)
  462. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  463. if !entering || ast.NodeLinkDest != n.Type {
  464. return ast.WalkContinue
  465. }
  466. dest := n.TokensStr()
  467. if !util.IsRelativePath(dest) || "" == dest {
  468. return ast.WalkContinue
  469. }
  470. absDest := filepath.Join(currentDir, dest)
  471. fullPath, exist := assets[absDest]
  472. if !exist {
  473. absDest = filepath.Join(currentDir, string(html.DecodeDestination([]byte(dest))))
  474. }
  475. fullPath, exist = assets[absDest]
  476. if exist {
  477. existName := assetsDone[absDest]
  478. var name string
  479. if "" == existName {
  480. name = filepath.Base(fullPath)
  481. name = util.AssetName(name)
  482. assetTargetPath := filepath.Join(assetDirPath, name)
  483. if err = gulu.File.Copy(fullPath, assetTargetPath); nil != err {
  484. logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", fullPath, assetTargetPath, err)
  485. return ast.WalkContinue
  486. }
  487. assetsDone[absDest] = name
  488. } else {
  489. name = existName
  490. }
  491. n.Tokens = []byte("assets/" + name)
  492. }
  493. return ast.WalkContinue
  494. })
  495. reassignIDUpdated(tree)
  496. if err = filesys.WriteTree(tree); nil != err {
  497. return io.EOF
  498. }
  499. i++
  500. if 0 == i%4 {
  501. util.PushEndlessProgress(fmt.Sprintf(Conf.Language(66), util.ShortPathForBootingDisplay(tree.Path)))
  502. }
  503. return nil
  504. })
  505. if nil != err {
  506. return err
  507. }
  508. IncSync()
  509. FullReindex()
  510. } else { // 导入单个文件
  511. fileName := filepath.Base(localPath)
  512. if !strings.HasSuffix(fileName, ".md") && !strings.HasSuffix(fileName, ".markdown") {
  513. return errors.New(Conf.Language(79))
  514. }
  515. title := strings.TrimSuffix(fileName, ".markdown")
  516. title = strings.TrimSuffix(title, ".md")
  517. targetPath := strings.TrimSuffix(toPath, ".sy")
  518. id := ast.NewNodeID()
  519. targetPath = path.Join(targetPath, id+".sy")
  520. var data []byte
  521. data, err = os.ReadFile(localPath)
  522. if nil != err {
  523. return err
  524. }
  525. tree := parseKTree(data)
  526. if nil == tree {
  527. msg := fmt.Sprintf("parse tree [%s] failed", localPath)
  528. logging.LogErrorf(msg)
  529. return errors.New(msg)
  530. }
  531. imgHtmlBlock2InlineImg(tree)
  532. tree.ID = id
  533. tree.Root.ID = id
  534. tree.Root.SetIALAttr("id", tree.Root.ID)
  535. tree.Root.SetIALAttr("title", title)
  536. tree.Box = boxID
  537. tree.Path = targetPath
  538. tree.HPath = path.Join(baseHPath, title)
  539. luteEngine.NestedInlines2FlattedSpans(tree)
  540. docDirLocalPath := filepath.Dir(filepath.Join(boxLocalPath, targetPath))
  541. assetDirPath := getAssetsDir(boxLocalPath, docDirLocalPath)
  542. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  543. if !entering || ast.NodeLinkDest != n.Type {
  544. return ast.WalkContinue
  545. }
  546. dest := n.TokensStr()
  547. if !util.IsRelativePath(dest) {
  548. return ast.WalkContinue
  549. }
  550. dest = filepath.ToSlash(dest)
  551. if "" == dest {
  552. return ast.WalkContinue
  553. }
  554. absolutePath := filepath.Join(filepath.Dir(localPath), dest)
  555. exist := gulu.File.IsExist(absolutePath)
  556. if !exist {
  557. absolutePath = filepath.Join(filepath.Dir(localPath), string(html.DecodeDestination([]byte(dest))))
  558. exist = gulu.File.IsExist(absolutePath)
  559. }
  560. if exist {
  561. name := filepath.Base(absolutePath)
  562. name = util.AssetName(name)
  563. assetTargetPath := filepath.Join(assetDirPath, name)
  564. if err = gulu.File.CopyFile(absolutePath, assetTargetPath); nil != err {
  565. logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", absolutePath, assetTargetPath, err)
  566. return ast.WalkContinue
  567. }
  568. n.Tokens = []byte("assets/" + name)
  569. }
  570. return ast.WalkContinue
  571. })
  572. reassignIDUpdated(tree)
  573. if err = indexWriteJSONQueue(tree); nil != err {
  574. return
  575. }
  576. IncSync()
  577. sql.WaitForWritingDatabase()
  578. util.PushEndlessProgress(Conf.Language(58))
  579. go func() {
  580. time.Sleep(2 * time.Second)
  581. util.ReloadUI()
  582. }()
  583. }
  584. debug.FreeOSMemory()
  585. IncSync()
  586. return
  587. }
  588. func imgHtmlBlock2InlineImg(tree *parse.Tree) {
  589. imgHtmlBlocks := map[*ast.Node]*html.Node{}
  590. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  591. if !entering {
  592. return ast.WalkContinue
  593. }
  594. if ast.NodeHTMLBlock == n.Type {
  595. htmlNodes, pErr := html.ParseFragment(bytes.NewReader(n.Tokens), &html.Node{Type: html.ElementNode})
  596. if nil != pErr {
  597. logging.LogErrorf("parse html block [%s] failed: %s", n.Tokens, pErr)
  598. return ast.WalkContinue
  599. }
  600. if 1 > len(htmlNodes) {
  601. return ast.WalkContinue
  602. }
  603. if atom.Img == htmlNodes[0].DataAtom {
  604. imgHtmlBlocks[n] = htmlNodes[0]
  605. }
  606. }
  607. return ast.WalkContinue
  608. })
  609. for n, htmlImg := range imgHtmlBlocks {
  610. src := domAttrValue(htmlImg, "src")
  611. alt := domAttrValue(htmlImg, "alt")
  612. title := domAttrValue(htmlImg, "title")
  613. p := &ast.Node{Type: ast.NodeParagraph, ID: n.ID}
  614. img := &ast.Node{Type: ast.NodeImage}
  615. p.AppendChild(img)
  616. img.AppendChild(&ast.Node{Type: ast.NodeBang})
  617. img.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
  618. img.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte(alt)})
  619. img.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
  620. img.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
  621. img.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(src)})
  622. if "" != title {
  623. img.AppendChild(&ast.Node{Type: ast.NodeLinkSpace})
  624. img.AppendChild(&ast.Node{Type: ast.NodeLinkTitle})
  625. }
  626. img.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
  627. n.InsertBefore(p)
  628. n.Unlink()
  629. }
  630. return
  631. }
  632. func reassignIDUpdated(tree *parse.Tree) {
  633. var blockCount int
  634. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  635. if !entering || "" == n.ID {
  636. return ast.WalkContinue
  637. }
  638. blockCount++
  639. return ast.WalkContinue
  640. })
  641. ids := make([]string, blockCount)
  642. min, _ := strconv.ParseInt(time.Now().Add(-1*time.Duration(blockCount)*time.Second).Format("20060102150405"), 10, 64)
  643. for i := 0; i < blockCount; i++ {
  644. ids[i] = newID(fmt.Sprintf("%d", min))
  645. min++
  646. }
  647. var i int
  648. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  649. if !entering || "" == n.ID {
  650. return ast.WalkContinue
  651. }
  652. n.ID = ids[i]
  653. n.SetIALAttr("id", n.ID)
  654. n.SetIALAttr("updated", util.TimeFromID(n.ID))
  655. i++
  656. return ast.WalkContinue
  657. })
  658. tree.ID = tree.Root.ID
  659. tree.Path = path.Join(path.Dir(tree.Path), tree.ID+".sy")
  660. tree.Root.SetIALAttr("id", tree.Root.ID)
  661. }
  662. func newID(t string) string {
  663. return t + "-" + randStr(7)
  664. }
  665. func randStr(length int) string {
  666. letter := []rune("abcdefghijklmnopqrstuvwxyz0123456789")
  667. b := make([]rune, length)
  668. for i := range b {
  669. b[i] = letter[rand.Intn(len(letter))]
  670. }
  671. return string(b)
  672. }
  673. func domAttrValue(n *html.Node, attrName string) string {
  674. if nil == n {
  675. return ""
  676. }
  677. for _, attr := range n.Attr {
  678. if attr.Key == attrName {
  679. return attr.Val
  680. }
  681. }
  682. return ""
  683. }