import.go 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271
  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. "encoding/base64"
  20. "encoding/json"
  21. "errors"
  22. "fmt"
  23. "github.com/siyuan-note/riff"
  24. "github.com/siyuan-note/siyuan/kernel/av"
  25. "image"
  26. "image/jpeg"
  27. "image/png"
  28. "io"
  29. "io/fs"
  30. "math/rand"
  31. "os"
  32. "path"
  33. "path/filepath"
  34. "runtime/debug"
  35. "sort"
  36. "strconv"
  37. "strings"
  38. "time"
  39. "github.com/88250/gulu"
  40. "github.com/88250/lute/ast"
  41. "github.com/88250/lute/html"
  42. "github.com/88250/lute/html/atom"
  43. "github.com/88250/lute/parse"
  44. "github.com/88250/lute/render"
  45. "github.com/siyuan-note/filelock"
  46. "github.com/siyuan-note/logging"
  47. "github.com/siyuan-note/siyuan/kernel/filesys"
  48. "github.com/siyuan-note/siyuan/kernel/sql"
  49. "github.com/siyuan-note/siyuan/kernel/treenode"
  50. "github.com/siyuan-note/siyuan/kernel/util"
  51. )
  52. func HTML2Markdown(htmlStr string) (markdown string, err error) {
  53. assetDirPath := filepath.Join(util.DataDir, "assets")
  54. luteEngine := util.NewLute()
  55. tree := luteEngine.HTML2Tree(htmlStr)
  56. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  57. if !entering || ast.NodeLinkDest != n.Type {
  58. return ast.WalkContinue
  59. }
  60. dest := n.TokensStr()
  61. if strings.HasPrefix(dest, "data:image") && strings.Contains(dest, ";base64,") {
  62. processBase64Img(n, dest, assetDirPath, err)
  63. return ast.WalkContinue
  64. }
  65. return ast.WalkContinue
  66. })
  67. var formatted []byte
  68. renderer := render.NewFormatRenderer(tree, luteEngine.RenderOptions)
  69. for nodeType, rendererFunc := range luteEngine.HTML2MdRendererFuncs {
  70. renderer.ExtRendererFuncs[nodeType] = rendererFunc
  71. }
  72. formatted = renderer.Render()
  73. markdown = gulu.Str.FromBytes(formatted)
  74. return
  75. }
  76. func ImportSY(zipPath, boxID, toPath string) (err error) {
  77. util.PushEndlessProgress(Conf.Language(73))
  78. defer util.ClearPushProgress(100)
  79. lockSync()
  80. defer unlockSync()
  81. baseName := filepath.Base(zipPath)
  82. ext := filepath.Ext(baseName)
  83. baseName = strings.TrimSuffix(baseName, ext)
  84. unzipPath := filepath.Join(filepath.Dir(zipPath), baseName+"-"+gulu.Rand.String(7))
  85. err = gulu.Zip.Unzip(zipPath, unzipPath)
  86. if nil != err {
  87. return
  88. }
  89. defer os.RemoveAll(unzipPath)
  90. var syPaths []string
  91. filelock.Walk(unzipPath, func(path string, info fs.FileInfo, err error) error {
  92. if nil != err {
  93. return err
  94. }
  95. if !info.IsDir() && strings.HasSuffix(info.Name(), ".sy") {
  96. syPaths = append(syPaths, path)
  97. }
  98. return nil
  99. })
  100. unzipRootPaths, err := filepath.Glob(unzipPath + "/*")
  101. if nil != err {
  102. return
  103. }
  104. if 1 != len(unzipRootPaths) {
  105. logging.LogErrorf("invalid .sy.zip")
  106. return errors.New(Conf.Language(199))
  107. }
  108. unzipRootPath := unzipRootPaths[0]
  109. name := filepath.Base(unzipRootPath)
  110. if strings.HasPrefix(name, "data-20") && len("data-20230321175442") == len(name) {
  111. return errors.New(Conf.Language(199))
  112. }
  113. luteEngine := util.NewLute()
  114. blockIDs := map[string]string{}
  115. avBlockIDs := map[string]string{}
  116. trees := map[string]*parse.Tree{}
  117. // 重新生成块 ID
  118. for _, syPath := range syPaths {
  119. data, readErr := os.ReadFile(syPath)
  120. if nil != readErr {
  121. logging.LogErrorf("read .sy [%s] failed: %s", syPath, readErr)
  122. err = readErr
  123. return
  124. }
  125. tree, _, parseErr := filesys.ParseJSON(data, luteEngine.ParseOptions)
  126. if nil != parseErr {
  127. logging.LogErrorf("parse .sy [%s] failed: %s", syPath, parseErr)
  128. err = parseErr
  129. return
  130. }
  131. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  132. if !entering || "" == n.ID {
  133. return ast.WalkContinue
  134. }
  135. // 新 ID 保留时间部分,仅修改随机值,避免时间变化导致更新时间早于创建时间
  136. // Keep original creation time when importing .sy.zip https://github.com/siyuan-note/siyuan/issues/9923
  137. newNodeID := util.TimeFromID(n.ID) + "-" + util.RandString(7)
  138. blockIDs[n.ID] = newNodeID
  139. oldNodeID := n.ID
  140. n.ID = newNodeID
  141. n.SetIALAttr("id", newNodeID)
  142. // 重新指向数据库属性值
  143. for _, kv := range n.KramdownIAL {
  144. if 2 > len(kv) {
  145. continue
  146. }
  147. if strings.HasPrefix(kv[0], av.NodeAttrNameAvs) {
  148. avBlockIDs[oldNodeID] = newNodeID
  149. }
  150. }
  151. return ast.WalkContinue
  152. })
  153. tree.ID = tree.Root.ID
  154. tree.Path = filepath.ToSlash(strings.TrimPrefix(syPath, unzipRootPath))
  155. trees[tree.ID] = tree
  156. }
  157. // 引用和嵌入指向重新生成的块 ID
  158. for _, tree := range trees {
  159. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  160. if !entering {
  161. return ast.WalkContinue
  162. }
  163. if treenode.IsBlockRef(n) {
  164. defID, _, _ := treenode.GetBlockRef(n)
  165. newDefID := blockIDs[defID]
  166. if "" != newDefID {
  167. n.TextMarkBlockRefID = newDefID
  168. }
  169. } else if ast.NodeTextMark == n.Type && n.IsTextMarkType("a") && strings.HasPrefix(n.TextMarkAHref, "siyuan://blocks/") {
  170. // Block hyperlinks do not point to regenerated block IDs when importing .sy.zip https://github.com/siyuan-note/siyuan/issues/9083
  171. defID := strings.TrimPrefix(n.TextMarkAHref, "siyuan://blocks/")
  172. newDefID := blockIDs[defID]
  173. if "" != newDefID {
  174. n.TextMarkAHref = "siyuan://blocks/" + newDefID
  175. }
  176. } else if ast.NodeBlockQueryEmbedScript == n.Type {
  177. for oldID, newID := range blockIDs {
  178. // 导入 `.sy.zip` 后查询嵌入块失效 https://github.com/siyuan-note/siyuan/issues/5316
  179. n.Tokens = bytes.ReplaceAll(n.Tokens, []byte(oldID), []byte(newID))
  180. }
  181. }
  182. return ast.WalkContinue
  183. })
  184. }
  185. // 将关联的数据库文件移动到 data/storage/av/ 下
  186. storage := filepath.Join(unzipRootPath, "storage")
  187. storageAvDir := filepath.Join(storage, "av")
  188. avIDs := map[string]string{}
  189. renameAvPaths := map[string]string{}
  190. if gulu.File.IsExist(storageAvDir) {
  191. // 重新生成数据库数据
  192. filelock.Walk(storageAvDir, func(path string, info fs.FileInfo, err error) error {
  193. if !strings.HasSuffix(path, ".json") || !ast.IsNodeIDPattern(strings.TrimSuffix(info.Name(), ".json")) {
  194. return nil
  195. }
  196. // 重命名数据库
  197. newAvID := ast.NewNodeID()
  198. oldAvID := strings.TrimSuffix(info.Name(), ".json")
  199. newPath := filepath.Join(filepath.Dir(path), newAvID+".json")
  200. renameAvPaths[path] = newPath
  201. avIDs[oldAvID] = newAvID
  202. return nil
  203. })
  204. // 重命名数据库文件
  205. for oldPath, newPath := range renameAvPaths {
  206. data, readErr := os.ReadFile(oldPath)
  207. if nil != readErr {
  208. logging.LogErrorf("read av file [%s] failed: %s", oldPath, readErr)
  209. return nil
  210. }
  211. // 将数据库文件中的块 ID 替换为新的块 ID
  212. newData := data
  213. for oldAvID, newAvID := range avIDs {
  214. for oldID, newID := range avBlockIDs {
  215. newData = bytes.ReplaceAll(newData, []byte(oldID), []byte(newID))
  216. }
  217. newData = bytes.ReplaceAll(newData, []byte(oldAvID), []byte(newAvID))
  218. }
  219. if !bytes.Equal(data, newData) {
  220. if writeErr := os.WriteFile(oldPath, newData, 0644); nil != writeErr {
  221. logging.LogErrorf("write av file [%s] failed: %s", oldPath, writeErr)
  222. return nil
  223. }
  224. }
  225. if err = os.Rename(oldPath, newPath); nil != err {
  226. logging.LogErrorf("rename av file from [%s] to [%s] failed: %s", oldPath, newPath, err)
  227. return
  228. }
  229. }
  230. targetStorageAvDir := filepath.Join(util.DataDir, "storage", "av")
  231. if copyErr := filelock.Copy(storageAvDir, targetStorageAvDir); nil != copyErr {
  232. logging.LogErrorf("copy storage av dir from [%s] to [%s] failed: %s", storageAvDir, targetStorageAvDir, copyErr)
  233. }
  234. // 重新指向数据库属性值
  235. for _, tree := range trees {
  236. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  237. if !entering || "" == n.ID {
  238. return ast.WalkContinue
  239. }
  240. ial := parse.IAL2Map(n.KramdownIAL)
  241. for k, v := range ial {
  242. if strings.HasPrefix(k, av.NodeAttrNameAvs) {
  243. newKey, newVal := k, v
  244. for oldAvID, newAvID := range avIDs {
  245. newKey = strings.ReplaceAll(newKey, oldAvID, newAvID)
  246. newVal = strings.ReplaceAll(newVal, oldAvID, newAvID)
  247. }
  248. n.RemoveIALAttr(k)
  249. n.SetIALAttr(newKey, newVal)
  250. }
  251. }
  252. if ast.NodeAttributeView == n.Type {
  253. n.AttributeViewID = avIDs[n.AttributeViewID]
  254. }
  255. return ast.WalkContinue
  256. })
  257. // 关联数据库和块
  258. avNodes := tree.Root.ChildrenByType(ast.NodeAttributeView)
  259. av.BatchUpsertBlockRel(avNodes)
  260. }
  261. }
  262. // 将关联的闪卡数据合并到默认卡包 data/storage/riff/20230218211946-2kw8jgx 中
  263. storageRiffDir := filepath.Join(storage, "riff")
  264. if gulu.File.IsExist(storageRiffDir) {
  265. deckToImport, loadErr := riff.LoadDeck(storageRiffDir, builtinDeckID, Conf.Flashcard.RequestRetention, Conf.Flashcard.MaximumInterval, Conf.Flashcard.Weights)
  266. if nil != loadErr {
  267. logging.LogErrorf("load deck [%s] failed: %s", name, loadErr)
  268. } else {
  269. deck := Decks[builtinDeckID]
  270. if nil == deck {
  271. var createErr error
  272. deck, createErr = createDeck0("Built-in Deck", builtinDeckID)
  273. if nil == createErr {
  274. Decks[deck.ID] = deck
  275. }
  276. }
  277. bIDs := deckToImport.GetBlockIDs()
  278. cards := deckToImport.GetCardsByBlockIDs(bIDs)
  279. for _, card := range cards {
  280. deck.AddCard(card.ID(), blockIDs[card.BlockID()])
  281. }
  282. if 0 < len(cards) {
  283. if saveErr := deck.Save(); nil != saveErr {
  284. logging.LogErrorf("save deck [%s] failed: %s", name, saveErr)
  285. }
  286. }
  287. }
  288. }
  289. // storage 文件夹已在上方处理,所以这里删除源 storage 文件夹,避免后面被拷贝到导入目录下 targetDir
  290. if removeErr := os.RemoveAll(storage); nil != removeErr {
  291. logging.LogErrorf("remove temp storage av dir failed: %s", removeErr)
  292. }
  293. // 写回 .sy
  294. for _, tree := range trees {
  295. syPath := filepath.Join(unzipRootPath, tree.Path)
  296. if "" == tree.Root.Spec {
  297. parse.NestedInlines2FlattedSpans(tree, false)
  298. tree.Root.Spec = "1"
  299. }
  300. renderer := render.NewJSONRenderer(tree, luteEngine.RenderOptions)
  301. data := renderer.Render()
  302. if !util.UseSingleLineSave {
  303. buf := bytes.Buffer{}
  304. buf.Grow(1024 * 1024 * 2)
  305. if err = json.Indent(&buf, data, "", "\t"); nil != err {
  306. return
  307. }
  308. data = buf.Bytes()
  309. }
  310. if err = os.WriteFile(syPath, data, 0644); nil != err {
  311. logging.LogErrorf("write .sy [%s] failed: %s", syPath, err)
  312. return
  313. }
  314. newSyPath := filepath.Join(filepath.Dir(syPath), tree.ID+".sy")
  315. if err = filelock.Rename(syPath, newSyPath); nil != err {
  316. logging.LogErrorf("rename .sy from [%s] to [%s] failed: %s", syPath, newSyPath, err)
  317. return
  318. }
  319. }
  320. // 合并 sort.json
  321. fullSortIDs := map[string]int{}
  322. sortIDs := map[string]int{}
  323. var sortData []byte
  324. var sortErr error
  325. sortPath := filepath.Join(unzipRootPath, ".siyuan", "sort.json")
  326. if filelock.IsExist(sortPath) {
  327. sortData, sortErr = filelock.ReadFile(sortPath)
  328. if nil != sortErr {
  329. logging.LogErrorf("read import sort conf failed: %s", sortErr)
  330. }
  331. if sortErr = gulu.JSON.UnmarshalJSON(sortData, &sortIDs); nil != sortErr {
  332. logging.LogErrorf("unmarshal sort conf failed: %s", sortErr)
  333. }
  334. boxSortPath := filepath.Join(util.DataDir, boxID, ".siyuan", "sort.json")
  335. if filelock.IsExist(boxSortPath) {
  336. sortData, sortErr = filelock.ReadFile(boxSortPath)
  337. if nil != sortErr {
  338. logging.LogErrorf("read box sort conf failed: %s", sortErr)
  339. }
  340. if sortErr = gulu.JSON.UnmarshalJSON(sortData, &fullSortIDs); nil != sortErr {
  341. logging.LogErrorf("unmarshal box sort conf failed: %s", sortErr)
  342. }
  343. }
  344. for oldID, sort := range sortIDs {
  345. if newID := blockIDs[oldID]; "" != newID {
  346. fullSortIDs[newID] = sort
  347. }
  348. }
  349. sortData, sortErr = gulu.JSON.MarshalJSON(fullSortIDs)
  350. if nil != sortErr {
  351. logging.LogErrorf("marshal box full sort conf failed: %s", sortErr)
  352. } else {
  353. sortErr = filelock.WriteFile(boxSortPath, sortData)
  354. if nil != sortErr {
  355. logging.LogErrorf("write box full sort conf failed: %s", sortErr)
  356. }
  357. }
  358. if removeErr := os.RemoveAll(sortPath); nil != removeErr {
  359. logging.LogErrorf("remove temp sort conf failed: %s", removeErr)
  360. }
  361. }
  362. // 重命名文件路径
  363. renamePaths := map[string]string{}
  364. filelock.Walk(unzipRootPath, func(path string, info fs.FileInfo, err error) error {
  365. if nil != err {
  366. return err
  367. }
  368. if info.IsDir() && ast.IsNodeIDPattern(info.Name()) {
  369. renamePaths[path] = path
  370. }
  371. return nil
  372. })
  373. for p, _ := range renamePaths {
  374. originalPath := p
  375. p = strings.TrimPrefix(p, unzipRootPath)
  376. p = filepath.ToSlash(p)
  377. parts := strings.Split(p, "/")
  378. buf := bytes.Buffer{}
  379. buf.WriteString("/")
  380. for i, part := range parts {
  381. if "" == part {
  382. continue
  383. }
  384. newNodeID := blockIDs[part]
  385. if "" != newNodeID {
  386. buf.WriteString(newNodeID)
  387. } else {
  388. buf.WriteString(part)
  389. }
  390. if i < len(parts)-1 {
  391. buf.WriteString("/")
  392. }
  393. }
  394. newPath := buf.String()
  395. renamePaths[originalPath] = filepath.Join(unzipRootPath, newPath)
  396. }
  397. var oldPaths []string
  398. for oldPath, _ := range renamePaths {
  399. oldPaths = append(oldPaths, oldPath)
  400. }
  401. sort.Slice(oldPaths, func(i, j int) bool {
  402. return strings.Count(oldPaths[i], string(os.PathSeparator)) < strings.Count(oldPaths[j], string(os.PathSeparator))
  403. })
  404. for i, oldPath := range oldPaths {
  405. newPath := renamePaths[oldPath]
  406. if err = filelock.Rename(oldPath, newPath); nil != err {
  407. logging.LogErrorf("rename path from [%s] to [%s] failed: %s", oldPath, renamePaths[oldPath], err)
  408. return errors.New("rename path failed")
  409. }
  410. delete(renamePaths, oldPath)
  411. var toRemoves []string
  412. newRenamedPaths := map[string]string{}
  413. for oldP, newP := range renamePaths {
  414. if strings.HasPrefix(oldP, oldPath) {
  415. renamedOldP := strings.Replace(oldP, oldPath, newPath, 1)
  416. newRenamedPaths[renamedOldP] = newP
  417. toRemoves = append(toRemoves, oldPath)
  418. }
  419. }
  420. for _, toRemove := range toRemoves {
  421. delete(renamePaths, toRemove)
  422. }
  423. for oldP, newP := range newRenamedPaths {
  424. renamePaths[oldP] = newP
  425. }
  426. for j := i + 1; j < len(oldPaths); j++ {
  427. if strings.HasPrefix(oldPaths[j], oldPath) {
  428. renamedOldP := strings.Replace(oldPaths[j], oldPath, newPath, 1)
  429. oldPaths[j] = renamedOldP
  430. }
  431. }
  432. }
  433. // 将包含的资源文件统一移动到 data/assets/ 下
  434. var assetsDirs []string
  435. filelock.Walk(unzipRootPath, func(path string, info fs.FileInfo, err error) error {
  436. if strings.Contains(path, "assets") && info.IsDir() {
  437. assetsDirs = append(assetsDirs, path)
  438. }
  439. return nil
  440. })
  441. dataAssets := filepath.Join(util.DataDir, "assets")
  442. for _, assets := range assetsDirs {
  443. if gulu.File.IsDir(assets) {
  444. if err = filelock.Copy(assets, dataAssets); nil != err {
  445. logging.LogErrorf("copy assets from [%s] to [%s] failed: %s", assets, dataAssets, err)
  446. return
  447. }
  448. }
  449. os.RemoveAll(assets)
  450. }
  451. var baseTargetPath string
  452. if "/" == toPath {
  453. baseTargetPath = "/"
  454. } else {
  455. block := treenode.GetBlockTreeRootByPath(boxID, toPath)
  456. if nil == block {
  457. logging.LogErrorf("not found block by path [%s]", toPath)
  458. return nil
  459. }
  460. baseTargetPath = strings.TrimSuffix(block.Path, ".sy")
  461. }
  462. targetDir := filepath.Join(util.DataDir, boxID, baseTargetPath)
  463. if err = os.MkdirAll(targetDir, 0755); nil != err {
  464. return
  465. }
  466. var treePaths []string
  467. filelock.Walk(unzipRootPath, func(path string, info fs.FileInfo, err error) error {
  468. if info.IsDir() {
  469. if strings.HasPrefix(info.Name(), ".") {
  470. return filepath.SkipDir
  471. }
  472. return nil
  473. }
  474. if !strings.HasSuffix(info.Name(), ".sy") {
  475. return nil
  476. }
  477. p := strings.TrimPrefix(path, unzipRootPath)
  478. p = filepath.ToSlash(p)
  479. treePaths = append(treePaths, p)
  480. return nil
  481. })
  482. if err = filelock.Copy(unzipRootPath, targetDir); nil != err {
  483. logging.LogErrorf("copy data dir from [%s] to [%s] failed: %s", unzipRootPath, util.DataDir, err)
  484. err = errors.New("copy data failed")
  485. return
  486. }
  487. boxAbsPath := filepath.Join(util.DataDir, boxID)
  488. for _, treePath := range treePaths {
  489. absPath := filepath.Join(targetDir, treePath)
  490. p := strings.TrimPrefix(absPath, boxAbsPath)
  491. p = filepath.ToSlash(p)
  492. tree, err := filesys.LoadTree(boxID, p, luteEngine)
  493. if nil != err {
  494. logging.LogErrorf("load tree [%s] failed: %s", treePath, err)
  495. continue
  496. }
  497. treenode.IndexBlockTree(tree)
  498. sql.IndexTreeQueue(tree)
  499. }
  500. IncSync()
  501. return
  502. }
  503. func ImportData(zipPath string) (err error) {
  504. util.PushEndlessProgress(Conf.Language(73))
  505. defer util.ClearPushProgress(100)
  506. lockSync()
  507. defer unlockSync()
  508. baseName := filepath.Base(zipPath)
  509. ext := filepath.Ext(baseName)
  510. baseName = strings.TrimSuffix(baseName, ext)
  511. unzipPath := filepath.Join(filepath.Dir(zipPath), baseName)
  512. err = gulu.Zip.Unzip(zipPath, unzipPath)
  513. if nil != err {
  514. return
  515. }
  516. defer os.RemoveAll(unzipPath)
  517. files, err := filepath.Glob(filepath.Join(unzipPath, "*/*.sy"))
  518. if nil != err {
  519. logging.LogErrorf("check data.zip failed: %s", err)
  520. return errors.New("check data.zip failed")
  521. }
  522. if 0 < len(files) {
  523. return errors.New(Conf.Language(198))
  524. }
  525. dirs, err := os.ReadDir(unzipPath)
  526. if nil != err {
  527. logging.LogErrorf("check data.zip failed: %s", err)
  528. return errors.New("check data.zip failed")
  529. }
  530. if 1 != len(dirs) {
  531. return errors.New(Conf.Language(198))
  532. }
  533. tmpDataPath := filepath.Join(unzipPath, dirs[0].Name())
  534. if err = filelock.Copy(tmpDataPath, util.DataDir); nil != err {
  535. logging.LogErrorf("copy data dir from [%s] to [%s] failed: %s", tmpDataPath, util.DataDir, err)
  536. err = errors.New("copy data failed")
  537. return
  538. }
  539. IncSync()
  540. FullReindex()
  541. return
  542. }
  543. func ImportFromLocalPath(boxID, localPath string, toPath string) (err error) {
  544. util.PushEndlessProgress(Conf.Language(73))
  545. defer func() {
  546. util.PushClearProgress()
  547. if e := recover(); nil != e {
  548. stack := debug.Stack()
  549. msg := fmt.Sprintf("PANIC RECOVERED: %v\n\t%s\n", e, stack)
  550. logging.LogErrorf("import from local path failed: %s", msg)
  551. err = errors.New("import from local path failed, please check kernel log for details")
  552. }
  553. }()
  554. lockSync()
  555. defer unlockSync()
  556. WaitForWritingFiles()
  557. var baseHPath, baseTargetPath, boxLocalPath string
  558. if "/" == toPath {
  559. baseHPath = "/"
  560. baseTargetPath = "/"
  561. } else {
  562. block := treenode.GetBlockTreeRootByPath(boxID, toPath)
  563. if nil == block {
  564. logging.LogErrorf("not found block by path [%s]", toPath)
  565. return nil
  566. }
  567. baseHPath = block.HPath
  568. baseTargetPath = strings.TrimSuffix(block.Path, ".sy")
  569. }
  570. boxLocalPath = filepath.Join(util.DataDir, boxID)
  571. if gulu.File.IsDir(localPath) {
  572. // 收集所有资源文件
  573. assets := map[string]string{}
  574. filelock.Walk(localPath, func(currentPath string, info os.FileInfo, walkErr error) error {
  575. if localPath == currentPath {
  576. return nil
  577. }
  578. if strings.HasPrefix(info.Name(), ".") {
  579. if info.IsDir() {
  580. return filepath.SkipDir
  581. }
  582. return nil
  583. }
  584. if !strings.HasSuffix(info.Name(), ".md") && !strings.HasSuffix(info.Name(), ".markdown") {
  585. assets[currentPath] = currentPath
  586. return nil
  587. }
  588. return nil
  589. })
  590. targetPaths := map[string]string{}
  591. assetsDone := map[string]string{}
  592. // md 转换 sy
  593. filelock.Walk(localPath, func(currentPath string, info os.FileInfo, walkErr error) error {
  594. if strings.HasPrefix(info.Name(), ".") {
  595. if info.IsDir() {
  596. return filepath.SkipDir
  597. }
  598. return nil
  599. }
  600. var tree *parse.Tree
  601. var ext string
  602. title := info.Name()
  603. if !info.IsDir() {
  604. ext = path.Ext(info.Name())
  605. title = strings.TrimSuffix(info.Name(), ext)
  606. }
  607. id := ast.NewNodeID()
  608. curRelPath := filepath.ToSlash(strings.TrimPrefix(currentPath, localPath))
  609. targetPath := path.Join(baseTargetPath, id)
  610. hPath := path.Join(baseHPath, filepath.ToSlash(strings.TrimPrefix(currentPath, localPath)))
  611. hPath = strings.TrimSuffix(hPath, ext)
  612. if "" == curRelPath {
  613. curRelPath = "/"
  614. hPath = "/" + title
  615. } else {
  616. dirPath := targetPaths[path.Dir(curRelPath)]
  617. targetPath = path.Join(dirPath, id)
  618. }
  619. targetPath = strings.ReplaceAll(targetPath, ".sy/", "/")
  620. targetPath += ".sy"
  621. targetPaths[curRelPath] = targetPath
  622. if info.IsDir() {
  623. tree = treenode.NewTree(boxID, targetPath, hPath, title)
  624. importTrees = append(importTrees, tree)
  625. return nil
  626. }
  627. if !strings.HasSuffix(info.Name(), ".md") && !strings.HasSuffix(info.Name(), ".markdown") {
  628. return nil
  629. }
  630. data, readErr := os.ReadFile(currentPath)
  631. if nil != readErr {
  632. err = readErr
  633. return io.EOF
  634. }
  635. tree = parseStdMd(data)
  636. if nil == tree {
  637. logging.LogErrorf("parse tree [%s] failed", currentPath)
  638. return nil
  639. }
  640. tree.ID = id
  641. tree.Root.ID = id
  642. tree.Root.SetIALAttr("id", tree.Root.ID)
  643. tree.Root.SetIALAttr("title", title)
  644. tree.Box = boxID
  645. targetPath = path.Join(path.Dir(targetPath), tree.Root.ID+".sy")
  646. tree.Path = targetPath
  647. targetPaths[curRelPath] = targetPath
  648. tree.HPath = hPath
  649. tree.Root.Spec = "1"
  650. docDirLocalPath := filepath.Dir(filepath.Join(boxLocalPath, targetPath))
  651. assetDirPath := getAssetsDir(boxLocalPath, docDirLocalPath)
  652. currentDir := filepath.Dir(currentPath)
  653. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  654. if !entering || (ast.NodeLinkDest != n.Type && !n.IsTextMarkType("a")) {
  655. return ast.WalkContinue
  656. }
  657. var dest string
  658. if ast.NodeLinkDest == n.Type {
  659. dest = n.TokensStr()
  660. } else {
  661. dest = n.TextMarkAHref
  662. }
  663. if strings.HasPrefix(dest, "data:image") && strings.Contains(dest, ";base64,") {
  664. processBase64Img(n, dest, assetDirPath, err)
  665. return ast.WalkContinue
  666. }
  667. dest = strings.ReplaceAll(dest, "%20", " ")
  668. dest = strings.ReplaceAll(dest, "%5C", "/")
  669. if ast.NodeLinkDest == n.Type {
  670. n.Tokens = []byte(dest)
  671. } else {
  672. n.TextMarkAHref = dest
  673. }
  674. if !util.IsRelativePath(dest) {
  675. return ast.WalkContinue
  676. }
  677. dest = filepath.ToSlash(dest)
  678. if "" == dest {
  679. return ast.WalkContinue
  680. }
  681. absDest := filepath.Join(currentDir, dest)
  682. fullPath, exist := assets[absDest]
  683. if !exist {
  684. absDest = filepath.Join(currentDir, string(html.DecodeDestination([]byte(dest))))
  685. }
  686. fullPath, exist = assets[absDest]
  687. if exist {
  688. existName := assetsDone[absDest]
  689. var name string
  690. if "" == existName {
  691. name = filepath.Base(fullPath)
  692. name = util.AssetName(name)
  693. assetTargetPath := filepath.Join(assetDirPath, name)
  694. if err = filelock.Copy(fullPath, assetTargetPath); nil != err {
  695. logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", fullPath, assetTargetPath, err)
  696. return ast.WalkContinue
  697. }
  698. assetsDone[absDest] = name
  699. } else {
  700. name = existName
  701. }
  702. if ast.NodeLinkDest == n.Type {
  703. n.Tokens = []byte("assets/" + name)
  704. } else {
  705. n.TextMarkAHref = "assets/" + name
  706. }
  707. }
  708. return ast.WalkContinue
  709. })
  710. reassignIDUpdated(tree)
  711. importTrees = append(importTrees, tree)
  712. return nil
  713. })
  714. } else { // 导入单个文件
  715. fileName := filepath.Base(localPath)
  716. if !strings.HasSuffix(fileName, ".md") && !strings.HasSuffix(fileName, ".markdown") {
  717. return errors.New(Conf.Language(79))
  718. }
  719. title := strings.TrimSuffix(fileName, ".markdown")
  720. title = strings.TrimSuffix(title, ".md")
  721. targetPath := strings.TrimSuffix(toPath, ".sy")
  722. id := ast.NewNodeID()
  723. targetPath = path.Join(targetPath, id+".sy")
  724. var data []byte
  725. data, err = os.ReadFile(localPath)
  726. if nil != err {
  727. return err
  728. }
  729. tree := parseStdMd(data)
  730. if nil == tree {
  731. msg := fmt.Sprintf("parse tree [%s] failed", localPath)
  732. logging.LogErrorf(msg)
  733. return errors.New(msg)
  734. }
  735. tree.ID = id
  736. tree.Root.ID = id
  737. tree.Root.SetIALAttr("id", tree.Root.ID)
  738. tree.Root.SetIALAttr("title", title)
  739. tree.Box = boxID
  740. tree.Path = targetPath
  741. tree.HPath = path.Join(baseHPath, title)
  742. tree.Root.Spec = "1"
  743. docDirLocalPath := filepath.Dir(filepath.Join(boxLocalPath, targetPath))
  744. assetDirPath := getAssetsDir(boxLocalPath, docDirLocalPath)
  745. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  746. if !entering || (ast.NodeLinkDest != n.Type && !n.IsTextMarkType("a")) {
  747. return ast.WalkContinue
  748. }
  749. var dest string
  750. if ast.NodeLinkDest == n.Type {
  751. dest = n.TokensStr()
  752. } else {
  753. dest = n.TextMarkAHref
  754. }
  755. if strings.HasPrefix(dest, "data:image") && strings.Contains(dest, ";base64,") {
  756. processBase64Img(n, dest, assetDirPath, err)
  757. return ast.WalkContinue
  758. }
  759. dest = strings.ReplaceAll(dest, "%20", " ")
  760. dest = strings.ReplaceAll(dest, "%5C", "/")
  761. if ast.NodeLinkDest == n.Type {
  762. n.Tokens = []byte(dest)
  763. } else {
  764. n.TextMarkAHref = dest
  765. }
  766. if !util.IsRelativePath(dest) {
  767. return ast.WalkContinue
  768. }
  769. dest = filepath.ToSlash(dest)
  770. if "" == dest {
  771. return ast.WalkContinue
  772. }
  773. absolutePath := filepath.Join(filepath.Dir(localPath), dest)
  774. exist := gulu.File.IsExist(absolutePath)
  775. if !exist {
  776. absolutePath = filepath.Join(filepath.Dir(localPath), string(html.DecodeDestination([]byte(dest))))
  777. exist = gulu.File.IsExist(absolutePath)
  778. }
  779. if exist {
  780. name := filepath.Base(absolutePath)
  781. name = util.AssetName(name)
  782. assetTargetPath := filepath.Join(assetDirPath, name)
  783. if err = filelock.Copy(absolutePath, assetTargetPath); nil != err {
  784. logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", absolutePath, assetTargetPath, err)
  785. return ast.WalkContinue
  786. }
  787. if ast.NodeLinkDest == n.Type {
  788. n.Tokens = []byte("assets/" + name)
  789. } else {
  790. n.TextMarkAHref = "assets/" + name
  791. }
  792. }
  793. return ast.WalkContinue
  794. })
  795. reassignIDUpdated(tree)
  796. importTrees = append(importTrees, tree)
  797. }
  798. if 0 < len(importTrees) {
  799. initSearchLinks()
  800. convertWikiLinksAndTags()
  801. buildBlockRefInText()
  802. for i, tree := range importTrees {
  803. indexWriteTreeIndexQueue(tree)
  804. if 0 == i%4 {
  805. util.PushEndlessProgress(fmt.Sprintf(Conf.Language(66), fmt.Sprintf("%d/%d ", i, len(importTrees))+tree.HPath))
  806. }
  807. }
  808. util.PushClearProgress()
  809. importTrees = []*parse.Tree{}
  810. searchLinks = map[string]string{}
  811. }
  812. IncSync()
  813. debug.FreeOSMemory()
  814. return
  815. }
  816. func parseStdMd(markdown []byte) (ret *parse.Tree) {
  817. luteEngine := util.NewStdLute()
  818. ret = parse.Parse("", markdown, luteEngine.ParseOptions)
  819. if nil == ret {
  820. return
  821. }
  822. genTreeID(ret)
  823. imgHtmlBlock2InlineImg(ret)
  824. parse.NestedInlines2FlattedSpansHybrid(ret, false)
  825. return
  826. }
  827. func processBase64Img(n *ast.Node, dest string, assetDirPath string, err error) {
  828. base64TmpDir := filepath.Join(util.TempDir, "base64")
  829. os.MkdirAll(base64TmpDir, 0755)
  830. sep := strings.Index(dest, ";base64,")
  831. var decodeErr error
  832. unbased, decodeErr := base64.StdEncoding.DecodeString(dest[sep+8:])
  833. if nil != decodeErr {
  834. logging.LogErrorf("decode base64 image failed: %s", decodeErr)
  835. return
  836. }
  837. dataReader := bytes.NewReader(unbased)
  838. var img image.Image
  839. var ext string
  840. typ := dest[5:sep]
  841. switch typ {
  842. case "image/png":
  843. img, decodeErr = png.Decode(dataReader)
  844. ext = ".png"
  845. case "image/jpeg":
  846. img, decodeErr = jpeg.Decode(dataReader)
  847. ext = ".jpg"
  848. default:
  849. logging.LogWarnf("unsupported base64 image type [%s]", typ)
  850. return
  851. }
  852. if nil != decodeErr {
  853. logging.LogErrorf("decode base64 image failed: %s", decodeErr)
  854. return
  855. }
  856. name := "image" + ext
  857. alt := n.Parent.ChildByType(ast.NodeLinkText)
  858. if nil != alt {
  859. name = alt.TokensStr() + ext
  860. }
  861. name = util.FilterUploadFileName(name)
  862. name = util.AssetName(name)
  863. tmp := filepath.Join(base64TmpDir, name)
  864. tmpFile, openErr := os.OpenFile(tmp, os.O_RDWR|os.O_CREATE, 0644)
  865. if nil != openErr {
  866. logging.LogErrorf("open temp file [%s] failed: %s", tmp, openErr)
  867. return
  868. }
  869. var encodeErr error
  870. switch typ {
  871. case "image/png":
  872. encodeErr = png.Encode(tmpFile, img)
  873. case "image/jpeg":
  874. encodeErr = jpeg.Encode(tmpFile, img, &jpeg.Options{Quality: 100})
  875. }
  876. if nil != encodeErr {
  877. logging.LogErrorf("encode base64 image failed: %s", encodeErr)
  878. tmpFile.Close()
  879. return
  880. }
  881. tmpFile.Close()
  882. assetTargetPath := filepath.Join(assetDirPath, name)
  883. if err = filelock.Copy(tmp, assetTargetPath); nil != err {
  884. logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", tmp, assetTargetPath, err)
  885. return
  886. }
  887. n.Tokens = []byte("assets/" + name)
  888. }
  889. func imgHtmlBlock2InlineImg(tree *parse.Tree) {
  890. imgHtmlBlocks := map[*ast.Node]*html.Node{}
  891. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  892. if !entering {
  893. return ast.WalkContinue
  894. }
  895. if ast.NodeHTMLBlock == n.Type {
  896. htmlNodes, pErr := html.ParseFragment(bytes.NewReader(n.Tokens), &html.Node{Type: html.ElementNode})
  897. if nil != pErr {
  898. logging.LogErrorf("parse html block [%s] failed: %s", n.Tokens, pErr)
  899. return ast.WalkContinue
  900. }
  901. if 1 > len(htmlNodes) {
  902. return ast.WalkContinue
  903. }
  904. for _, htmlNode := range htmlNodes {
  905. if atom.Img == htmlNode.DataAtom {
  906. imgHtmlBlocks[n] = htmlNode
  907. break
  908. }
  909. }
  910. }
  911. return ast.WalkContinue
  912. })
  913. for n, htmlImg := range imgHtmlBlocks {
  914. src := domAttrValue(htmlImg, "src")
  915. alt := domAttrValue(htmlImg, "alt")
  916. title := domAttrValue(htmlImg, "title")
  917. p := &ast.Node{Type: ast.NodeParagraph, ID: n.ID}
  918. img := &ast.Node{Type: ast.NodeImage}
  919. p.AppendChild(img)
  920. img.AppendChild(&ast.Node{Type: ast.NodeBang})
  921. img.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
  922. img.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte(alt)})
  923. img.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
  924. img.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
  925. img.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(src)})
  926. if "" != title {
  927. img.AppendChild(&ast.Node{Type: ast.NodeLinkSpace})
  928. img.AppendChild(&ast.Node{Type: ast.NodeLinkTitle})
  929. }
  930. img.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
  931. n.InsertBefore(p)
  932. n.Unlink()
  933. }
  934. return
  935. }
  936. func reassignIDUpdated(tree *parse.Tree) {
  937. var blockCount int
  938. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  939. if !entering || "" == n.ID {
  940. return ast.WalkContinue
  941. }
  942. blockCount++
  943. return ast.WalkContinue
  944. })
  945. ids := make([]string, blockCount)
  946. min, _ := strconv.ParseInt(time.Now().Add(-1*time.Duration(blockCount)*time.Second).Format("20060102150405"), 10, 64)
  947. for i := 0; i < blockCount; i++ {
  948. ids[i] = newID(fmt.Sprintf("%d", min))
  949. min++
  950. }
  951. var i int
  952. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  953. if !entering || "" == n.ID {
  954. return ast.WalkContinue
  955. }
  956. n.ID = ids[i]
  957. n.SetIALAttr("id", n.ID)
  958. n.SetIALAttr("updated", util.TimeFromID(n.ID))
  959. i++
  960. return ast.WalkContinue
  961. })
  962. tree.ID = tree.Root.ID
  963. tree.Path = path.Join(path.Dir(tree.Path), tree.ID+".sy")
  964. tree.Root.SetIALAttr("id", tree.Root.ID)
  965. }
  966. func newID(t string) string {
  967. return t + "-" + randStr(7)
  968. }
  969. func randStr(length int) string {
  970. letter := []rune("abcdefghijklmnopqrstuvwxyz0123456789")
  971. b := make([]rune, length)
  972. for i := range b {
  973. b[i] = letter[rand.Intn(len(letter))]
  974. }
  975. return string(b)
  976. }
  977. func domAttrValue(n *html.Node, attrName string) string {
  978. if nil == n {
  979. return ""
  980. }
  981. for _, attr := range n.Attr {
  982. if attr.Key == attrName {
  983. return attr.Val
  984. }
  985. }
  986. return ""
  987. }
  988. var importTrees []*parse.Tree
  989. var searchLinks = map[string]string{}
  990. func initSearchLinks() {
  991. for _, tree := range importTrees {
  992. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  993. if !entering || (ast.NodeDocument != n.Type && ast.NodeHeading != n.Type) {
  994. return ast.WalkContinue
  995. }
  996. nodePath := tree.HPath + "#"
  997. if ast.NodeHeading == n.Type {
  998. nodePath += n.Text()
  999. }
  1000. searchLinks[nodePath] = n.ID
  1001. return ast.WalkContinue
  1002. })
  1003. }
  1004. }
  1005. func convertWikiLinksAndTags() {
  1006. for _, tree := range importTrees {
  1007. convertWikiLinksAndTags0(tree)
  1008. }
  1009. }
  1010. func convertWikiLinksAndTags0(tree *parse.Tree) {
  1011. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  1012. if !entering || ast.NodeText != n.Type {
  1013. return ast.WalkContinue
  1014. }
  1015. text := n.TokensStr()
  1016. length := len(text)
  1017. start, end := 0, length
  1018. for {
  1019. part := text[start:end]
  1020. if idx := strings.Index(part, "]]"); 0 > idx {
  1021. break
  1022. } else {
  1023. end = start + idx
  1024. }
  1025. if idx := strings.Index(part, "[["); 0 > idx {
  1026. break
  1027. } else {
  1028. start += idx
  1029. }
  1030. if end <= start {
  1031. break
  1032. }
  1033. link := path.Join(path.Dir(tree.HPath), text[start+2:end]) // 统一转为绝对路径方便后续查找
  1034. linkText := path.Base(link)
  1035. dynamicAnchorText := true
  1036. if linkParts := strings.Split(link, "|"); 1 < len(linkParts) {
  1037. link = linkParts[0]
  1038. linkText = linkParts[1]
  1039. dynamicAnchorText = false
  1040. }
  1041. link, linkText = strings.TrimSpace(link), strings.TrimSpace(linkText)
  1042. if !strings.Contains(link, "#") {
  1043. link += "#" // 在结尾统一带上锚点方便后续查找
  1044. }
  1045. id := searchLinkID(link)
  1046. if "" == id {
  1047. start, end = end, length
  1048. continue
  1049. }
  1050. linkText = strings.TrimPrefix(linkText, "/")
  1051. repl := "((" + id + " '" + linkText + "'))"
  1052. if !dynamicAnchorText {
  1053. repl = "((" + id + " \"" + linkText + "\"))"
  1054. }
  1055. end += 2
  1056. text = text[:start] + repl + text[end:]
  1057. start, end = start+len(repl), len(text)
  1058. length = end
  1059. }
  1060. text = convertTags(text) // 导入标签语法
  1061. n.Tokens = gulu.Str.ToBytes(text)
  1062. return ast.WalkContinue
  1063. })
  1064. }
  1065. func convertTags(text string) (ret string) {
  1066. pos, i := -1, 0
  1067. tokens := []byte(text)
  1068. for ; i < len(tokens); i++ {
  1069. if '#' == tokens[i] && (0 == i || ' ' == tokens[i-1] || (-1 < pos && '#' == tokens[pos])) {
  1070. if i < len(tokens)-1 && '#' == tokens[i+1] {
  1071. pos = -1
  1072. continue
  1073. }
  1074. pos = i
  1075. continue
  1076. }
  1077. if -1 < pos && ' ' == tokens[i] {
  1078. tokens = append(tokens, 0)
  1079. copy(tokens[i+1:], tokens[i:])
  1080. tokens[i] = '#'
  1081. pos = -1
  1082. i++
  1083. }
  1084. }
  1085. if -1 < pos && pos < i {
  1086. tokens = append(tokens, '#')
  1087. }
  1088. return string(tokens)
  1089. }
  1090. // buildBlockRefInText 将文本节点进行结构化处理。
  1091. func buildBlockRefInText() {
  1092. lute := NewLute()
  1093. lute.SetHTMLTag2TextMark(true)
  1094. for _, tree := range importTrees {
  1095. tree.MergeText()
  1096. var unlinkTextNodes []*ast.Node
  1097. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  1098. if !entering || ast.NodeText != n.Type {
  1099. return ast.WalkContinue
  1100. }
  1101. if nil == n.Tokens {
  1102. return ast.WalkContinue
  1103. }
  1104. t := parse.Inline("", n.Tokens, lute.ParseOptions) // 使用行级解析
  1105. parse.NestedInlines2FlattedSpans(t, false)
  1106. var children []*ast.Node
  1107. for c := t.Root.FirstChild.FirstChild; nil != c; c = c.Next {
  1108. children = append(children, c)
  1109. }
  1110. for _, c := range children {
  1111. n.InsertBefore(c)
  1112. }
  1113. unlinkTextNodes = append(unlinkTextNodes, n)
  1114. return ast.WalkContinue
  1115. })
  1116. for _, node := range unlinkTextNodes {
  1117. node.Unlink()
  1118. }
  1119. }
  1120. }
  1121. func searchLinkID(link string) (id string) {
  1122. id = searchLinks[link]
  1123. if "" != id {
  1124. return
  1125. }
  1126. baseName := path.Base(link)
  1127. for searchLink, searchID := range searchLinks {
  1128. if path.Base(searchLink) == baseName {
  1129. return searchID
  1130. }
  1131. }
  1132. return
  1133. }