mirror of
https://github.com/siyuan-note/siyuan.git
synced 2025-05-03 05:21:10 +08:00
1443 lines
41 KiB
Go
1443 lines
41 KiB
Go
// SiYuan - Refactor your thinking
|
|
// Copyright (c) 2020-present, b3log.org
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package model
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"io/fs"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime/debug"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/88250/gulu"
|
|
"github.com/88250/lute"
|
|
"github.com/88250/lute/ast"
|
|
"github.com/88250/lute/html"
|
|
"github.com/88250/lute/html/atom"
|
|
"github.com/88250/lute/parse"
|
|
"github.com/88250/lute/render"
|
|
"github.com/siyuan-note/filelock"
|
|
"github.com/siyuan-note/logging"
|
|
"github.com/siyuan-note/riff"
|
|
"github.com/siyuan-note/siyuan/kernel/av"
|
|
"github.com/siyuan-note/siyuan/kernel/filesys"
|
|
"github.com/siyuan-note/siyuan/kernel/sql"
|
|
"github.com/siyuan-note/siyuan/kernel/task"
|
|
"github.com/siyuan-note/siyuan/kernel/treenode"
|
|
"github.com/siyuan-note/siyuan/kernel/util"
|
|
)
|
|
|
|
func HTML2Markdown(htmlStr string, luteEngine *lute.Lute) (markdown string, withMath bool, err error) {
|
|
tree, withMath := HTML2Tree(htmlStr, luteEngine)
|
|
|
|
var formatted []byte
|
|
renderer := render.NewFormatRenderer(tree, luteEngine.RenderOptions)
|
|
for nodeType, rendererFunc := range luteEngine.HTML2MdRendererFuncs {
|
|
renderer.ExtRendererFuncs[nodeType] = rendererFunc
|
|
}
|
|
formatted = renderer.Render()
|
|
markdown = gulu.Str.FromBytes(formatted)
|
|
return
|
|
}
|
|
|
|
func HTML2Tree(htmlStr string, luteEngine *lute.Lute) (tree *parse.Tree, withMath bool) {
|
|
htmlStr = gulu.Str.RemovePUA(htmlStr)
|
|
assetDirPath := filepath.Join(util.DataDir, "assets")
|
|
tree = luteEngine.HTML2Tree(htmlStr)
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
if ast.NodeText == n.Type {
|
|
if n.ParentIs(ast.NodeTableCell) {
|
|
n.Tokens = bytes.ReplaceAll(n.Tokens, []byte("\\|"), []byte("|"))
|
|
n.Tokens = bytes.ReplaceAll(n.Tokens, []byte("|"), []byte("\\|"))
|
|
}
|
|
}
|
|
|
|
if ast.NodeInlineMath == n.Type {
|
|
withMath = true
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
if ast.NodeLinkDest != n.Type {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
dest := n.TokensStr()
|
|
if strings.HasPrefix(dest, "data:image") && strings.Contains(dest, ";base64,") {
|
|
processBase64Img(n, dest, assetDirPath)
|
|
return ast.WalkContinue
|
|
}
|
|
return ast.WalkContinue
|
|
})
|
|
return
|
|
}
|
|
|
|
func ImportSY(zipPath, boxID, toPath string) (err error) {
|
|
util.PushEndlessProgress(Conf.Language(73))
|
|
defer util.ClearPushProgress(100)
|
|
|
|
lockSync()
|
|
defer unlockSync()
|
|
|
|
baseName := filepath.Base(zipPath)
|
|
ext := filepath.Ext(baseName)
|
|
baseName = strings.TrimSuffix(baseName, ext)
|
|
unzipPath := filepath.Join(filepath.Dir(zipPath), baseName+"-"+gulu.Rand.String(7))
|
|
err = gulu.Zip.Unzip(zipPath, unzipPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer os.RemoveAll(unzipPath)
|
|
|
|
var syPaths []string
|
|
filelock.Walk(unzipPath, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !d.IsDir() && strings.HasSuffix(d.Name(), ".sy") {
|
|
syPaths = append(syPaths, path)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
entries, err := os.ReadDir(unzipPath)
|
|
if err != nil {
|
|
logging.LogErrorf("read unzip dir [%s] failed: %s", unzipPath, err)
|
|
return
|
|
}
|
|
if 1 != len(entries) {
|
|
logging.LogErrorf("invalid .sy.zip [%v]", entries)
|
|
return errors.New(Conf.Language(199))
|
|
}
|
|
unzipRootPath := filepath.Join(unzipPath, entries[0].Name())
|
|
name := filepath.Base(unzipRootPath)
|
|
if strings.HasPrefix(name, "data-20") && len("data-20230321175442") == len(name) {
|
|
logging.LogErrorf("invalid .sy.zip [unzipRootPath=%s, baseName=%s]", unzipRootPath, name)
|
|
return errors.New(Conf.Language(199))
|
|
}
|
|
|
|
luteEngine := util.NewLute()
|
|
blockIDs := map[string]string{}
|
|
trees := map[string]*parse.Tree{}
|
|
|
|
// 重新生成块 ID
|
|
for i, syPath := range syPaths {
|
|
data, readErr := os.ReadFile(syPath)
|
|
if nil != readErr {
|
|
logging.LogErrorf("read .sy [%s] failed: %s", syPath, readErr)
|
|
err = readErr
|
|
return
|
|
}
|
|
tree, _, parseErr := filesys.ParseJSON(data, luteEngine.ParseOptions)
|
|
if nil != parseErr {
|
|
logging.LogErrorf("parse .sy [%s] failed: %s", syPath, parseErr)
|
|
err = parseErr
|
|
return
|
|
}
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering || "" == n.ID {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
// 新 ID 保留时间部分,仅修改随机值,避免时间变化导致更新时间早于创建时间
|
|
// Keep original creation time when importing .sy.zip https://github.com/siyuan-note/siyuan/issues/9923
|
|
newNodeID := util.TimeFromID(n.ID) + "-" + util.RandString(7)
|
|
blockIDs[n.ID] = newNodeID
|
|
n.ID = newNodeID
|
|
n.SetIALAttr("id", newNodeID)
|
|
return ast.WalkContinue
|
|
})
|
|
tree.ID = tree.Root.ID
|
|
tree.Path = filepath.ToSlash(strings.TrimPrefix(syPath, unzipRootPath))
|
|
trees[tree.ID] = tree
|
|
util.PushEndlessProgress(Conf.language(73) + " " + fmt.Sprintf(Conf.language(70), fmt.Sprintf("%d/%d", i+1, len(syPaths))))
|
|
}
|
|
|
|
// 引用和嵌入指向重新生成的块 ID
|
|
for _, tree := range trees {
|
|
util.PushEndlessProgress(Conf.language(73) + " " + fmt.Sprintf(Conf.language(70), tree.Root.IALAttr("title")))
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
if treenode.IsBlockRef(n) {
|
|
defID, _, _ := treenode.GetBlockRef(n)
|
|
newDefID := blockIDs[defID]
|
|
if "" != newDefID {
|
|
n.TextMarkBlockRefID = newDefID
|
|
}
|
|
} else if ast.NodeTextMark == n.Type && n.IsTextMarkType("a") && strings.HasPrefix(n.TextMarkAHref, "siyuan://blocks/") {
|
|
// Block hyperlinks do not point to regenerated block IDs when importing .sy.zip https://github.com/siyuan-note/siyuan/issues/9083
|
|
defID := strings.TrimPrefix(n.TextMarkAHref, "siyuan://blocks/")
|
|
newDefID := blockIDs[defID]
|
|
if "" != newDefID {
|
|
n.TextMarkAHref = "siyuan://blocks/" + newDefID
|
|
}
|
|
} else if ast.NodeBlockQueryEmbedScript == n.Type {
|
|
for oldID, newID := range blockIDs {
|
|
// 导入 `.sy.zip` 后查询嵌入块失效 https://github.com/siyuan-note/siyuan/issues/5316
|
|
n.Tokens = bytes.ReplaceAll(n.Tokens, []byte(oldID), []byte(newID))
|
|
}
|
|
}
|
|
return ast.WalkContinue
|
|
})
|
|
}
|
|
|
|
var replacements []string
|
|
for oldID, newID := range blockIDs {
|
|
replacements = append(replacements, oldID, newID)
|
|
}
|
|
blockIDReplacer := strings.NewReplacer(replacements...)
|
|
|
|
// 将关联的数据库文件移动到 data/storage/av/ 下
|
|
storage := filepath.Join(unzipRootPath, "storage")
|
|
storageAvDir := filepath.Join(storage, "av")
|
|
avIDs := map[string]string{}
|
|
renameAvPaths := map[string]string{}
|
|
if gulu.File.IsExist(storageAvDir) {
|
|
// 重新生成数据库数据
|
|
filelock.Walk(storageAvDir, func(path string, d fs.DirEntry, err error) error {
|
|
if !strings.HasSuffix(path, ".json") || !ast.IsNodeIDPattern(strings.TrimSuffix(d.Name(), ".json")) {
|
|
return nil
|
|
}
|
|
|
|
// 重命名数据库
|
|
newAvID := ast.NewNodeID()
|
|
oldAvID := strings.TrimSuffix(d.Name(), ".json")
|
|
newPath := filepath.Join(filepath.Dir(path), newAvID+".json")
|
|
renameAvPaths[path] = newPath
|
|
avIDs[oldAvID] = newAvID
|
|
return nil
|
|
})
|
|
|
|
// 重命名数据库文件
|
|
for oldPath, newPath := range renameAvPaths {
|
|
data, readErr := os.ReadFile(oldPath)
|
|
if nil != readErr {
|
|
logging.LogErrorf("read av file [%s] failed: %s", oldPath, readErr)
|
|
return nil
|
|
}
|
|
|
|
// 将数据库文件中的 ID 替换为新的 ID
|
|
newData := data
|
|
for oldAvID, newAvID := range avIDs {
|
|
newData = bytes.ReplaceAll(newData, []byte(oldAvID), []byte(newAvID))
|
|
}
|
|
newData = []byte(blockIDReplacer.Replace(string(newData)))
|
|
if !bytes.Equal(data, newData) {
|
|
if writeErr := os.WriteFile(oldPath, newData, 0644); nil != writeErr {
|
|
logging.LogErrorf("write av file [%s] failed: %s", oldPath, writeErr)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if err = os.Rename(oldPath, newPath); err != nil {
|
|
logging.LogErrorf("rename av file from [%s] to [%s] failed: %s", oldPath, newPath, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
targetStorageAvDir := filepath.Join(util.DataDir, "storage", "av")
|
|
if copyErr := filelock.Copy(storageAvDir, targetStorageAvDir); nil != copyErr {
|
|
logging.LogErrorf("copy storage av dir from [%s] to [%s] failed: %s", storageAvDir, targetStorageAvDir, copyErr)
|
|
}
|
|
|
|
// 重新指向数据库属性值
|
|
for _, tree := range trees {
|
|
util.PushEndlessProgress(Conf.language(73) + " " + fmt.Sprintf(Conf.language(70), tree.Root.IALAttr("title")))
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering || "" == n.ID {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
ial := parse.IAL2Map(n.KramdownIAL)
|
|
for k, v := range ial {
|
|
if strings.HasPrefix(k, av.NodeAttrNameAvs) {
|
|
newKey, newVal := k, v
|
|
for oldAvID, newAvID := range avIDs {
|
|
newKey = strings.ReplaceAll(newKey, oldAvID, newAvID)
|
|
newVal = strings.ReplaceAll(newVal, oldAvID, newAvID)
|
|
}
|
|
n.RemoveIALAttr(k)
|
|
n.SetIALAttr(newKey, newVal)
|
|
}
|
|
}
|
|
|
|
if ast.NodeAttributeView == n.Type {
|
|
n.AttributeViewID = avIDs[n.AttributeViewID]
|
|
}
|
|
return ast.WalkContinue
|
|
})
|
|
|
|
// 关联数据库和块
|
|
avNodes := tree.Root.ChildrenByType(ast.NodeAttributeView)
|
|
av.BatchUpsertBlockRel(avNodes)
|
|
}
|
|
|
|
// 如果数据库中绑定的块不在导入的文档中,则需要单独更新这些绑定块的属性
|
|
var attrViewIDs []string
|
|
for _, avID := range avIDs {
|
|
attrViewIDs = append(attrViewIDs, avID)
|
|
}
|
|
updateBoundBlockAvsAttribute(attrViewIDs)
|
|
|
|
// 插入关联关系 https://github.com/siyuan-note/siyuan/issues/11628
|
|
relationAvs := map[string]string{}
|
|
for _, avID := range avIDs {
|
|
attrView, _ := av.ParseAttributeView(avID)
|
|
if nil == attrView {
|
|
continue
|
|
}
|
|
|
|
for _, keyValues := range attrView.KeyValues {
|
|
if nil != keyValues.Key && av.KeyTypeRelation == keyValues.Key.Type && nil != keyValues.Key.Relation {
|
|
relationAvs[avID] = keyValues.Key.Relation.AvID
|
|
}
|
|
}
|
|
}
|
|
|
|
for srcAvID, destAvID := range relationAvs {
|
|
av.UpsertAvBackRel(srcAvID, destAvID)
|
|
}
|
|
}
|
|
|
|
// 将关联的闪卡数据合并到默认卡包 data/storage/riff/20230218211946-2kw8jgx 中
|
|
storageRiffDir := filepath.Join(storage, "riff")
|
|
if gulu.File.IsExist(storageRiffDir) {
|
|
deckToImport, loadErr := riff.LoadDeck(storageRiffDir, builtinDeckID, Conf.Flashcard.RequestRetention, Conf.Flashcard.MaximumInterval, Conf.Flashcard.Weights)
|
|
if nil != loadErr {
|
|
logging.LogErrorf("load deck [%s] failed: %s", name, loadErr)
|
|
} else {
|
|
deck := Decks[builtinDeckID]
|
|
if nil == deck {
|
|
var createErr error
|
|
deck, createErr = createDeck0("Built-in Deck", builtinDeckID)
|
|
if nil == createErr {
|
|
Decks[deck.ID] = deck
|
|
}
|
|
}
|
|
|
|
bIDs := deckToImport.GetBlockIDs()
|
|
cards := deckToImport.GetCardsByBlockIDs(bIDs)
|
|
for _, card := range cards {
|
|
deck.AddCard(card.ID(), blockIDs[card.BlockID()])
|
|
}
|
|
|
|
if 0 < len(cards) {
|
|
if saveErr := deck.Save(); nil != saveErr {
|
|
logging.LogErrorf("save deck [%s] failed: %s", name, saveErr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// storage 文件夹已在上方处理,所以这里删除源 storage 文件夹,避免后面被拷贝到导入目录下 targetDir
|
|
if removeErr := os.RemoveAll(storage); nil != removeErr {
|
|
logging.LogErrorf("remove temp storage av dir failed: %s", removeErr)
|
|
}
|
|
|
|
if 1 > len(avIDs) { // 如果本次没有导入数据库,则清理掉文档中的数据库属性 https://github.com/siyuan-note/siyuan/issues/13011
|
|
for _, tree := range trees {
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering || !n.IsBlock() {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
n.RemoveIALAttr(av.NodeAttrNameAvs)
|
|
return ast.WalkContinue
|
|
})
|
|
}
|
|
}
|
|
|
|
// 写回 .sy
|
|
for _, tree := range trees {
|
|
util.PushEndlessProgress(Conf.language(73) + " " + fmt.Sprintf(Conf.language(70), tree.Root.IALAttr("title")))
|
|
syPath := filepath.Join(unzipRootPath, tree.Path)
|
|
if "" == tree.Root.Spec {
|
|
parse.NestedInlines2FlattedSpans(tree, false)
|
|
tree.Root.Spec = "1"
|
|
}
|
|
renderer := render.NewJSONRenderer(tree, luteEngine.RenderOptions)
|
|
data := renderer.Render()
|
|
|
|
if !util.UseSingleLineSave {
|
|
buf := bytes.Buffer{}
|
|
buf.Grow(1024 * 1024 * 2)
|
|
if err = json.Indent(&buf, data, "", "\t"); err != nil {
|
|
return
|
|
}
|
|
data = buf.Bytes()
|
|
}
|
|
|
|
if err = os.WriteFile(syPath, data, 0644); err != nil {
|
|
logging.LogErrorf("write .sy [%s] failed: %s", syPath, err)
|
|
return
|
|
}
|
|
newSyPath := filepath.Join(filepath.Dir(syPath), tree.ID+".sy")
|
|
if err = filelock.Rename(syPath, newSyPath); err != nil {
|
|
logging.LogErrorf("rename .sy from [%s] to [%s] failed: %s", syPath, newSyPath, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// 合并 sort.json
|
|
fullSortIDs := map[string]int{}
|
|
sortIDs := map[string]int{}
|
|
var sortData []byte
|
|
var sortErr error
|
|
sortPath := filepath.Join(unzipRootPath, ".siyuan", "sort.json")
|
|
if filelock.IsExist(sortPath) {
|
|
sortData, sortErr = filelock.ReadFile(sortPath)
|
|
if nil != sortErr {
|
|
logging.LogErrorf("read import sort conf failed: %s", sortErr)
|
|
}
|
|
|
|
if sortErr = gulu.JSON.UnmarshalJSON(sortData, &sortIDs); nil != sortErr {
|
|
logging.LogErrorf("unmarshal sort conf failed: %s", sortErr)
|
|
}
|
|
|
|
boxSortPath := filepath.Join(util.DataDir, boxID, ".siyuan", "sort.json")
|
|
if filelock.IsExist(boxSortPath) {
|
|
sortData, sortErr = filelock.ReadFile(boxSortPath)
|
|
if nil != sortErr {
|
|
logging.LogErrorf("read box sort conf failed: %s", sortErr)
|
|
}
|
|
|
|
if sortErr = gulu.JSON.UnmarshalJSON(sortData, &fullSortIDs); nil != sortErr {
|
|
logging.LogErrorf("unmarshal box sort conf failed: %s", sortErr)
|
|
}
|
|
}
|
|
|
|
for oldID, sort := range sortIDs {
|
|
if newID := blockIDs[oldID]; "" != newID {
|
|
fullSortIDs[newID] = sort
|
|
}
|
|
}
|
|
|
|
sortData, sortErr = gulu.JSON.MarshalJSON(fullSortIDs)
|
|
if nil != sortErr {
|
|
logging.LogErrorf("marshal box full sort conf failed: %s", sortErr)
|
|
} else {
|
|
sortErr = filelock.WriteFile(boxSortPath, sortData)
|
|
if nil != sortErr {
|
|
logging.LogErrorf("write box full sort conf failed: %s", sortErr)
|
|
}
|
|
}
|
|
if removeErr := os.RemoveAll(sortPath); nil != removeErr {
|
|
logging.LogErrorf("remove temp sort conf failed: %s", removeErr)
|
|
}
|
|
}
|
|
|
|
// 重命名文件路径
|
|
renamePaths := map[string]string{}
|
|
filelock.Walk(unzipRootPath, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if d.IsDir() && ast.IsNodeIDPattern(d.Name()) {
|
|
renamePaths[path] = path
|
|
}
|
|
return nil
|
|
})
|
|
for p, _ := range renamePaths {
|
|
originalPath := p
|
|
p = strings.TrimPrefix(p, unzipRootPath)
|
|
p = filepath.ToSlash(p)
|
|
parts := strings.Split(p, "/")
|
|
buf := bytes.Buffer{}
|
|
buf.WriteString("/")
|
|
for i, part := range parts {
|
|
if "" == part {
|
|
continue
|
|
}
|
|
newNodeID := blockIDs[part]
|
|
if "" != newNodeID {
|
|
buf.WriteString(newNodeID)
|
|
} else {
|
|
buf.WriteString(part)
|
|
}
|
|
if i < len(parts)-1 {
|
|
buf.WriteString("/")
|
|
}
|
|
}
|
|
newPath := buf.String()
|
|
renamePaths[originalPath] = filepath.Join(unzipRootPath, newPath)
|
|
}
|
|
|
|
var oldPaths []string
|
|
for oldPath, _ := range renamePaths {
|
|
oldPaths = append(oldPaths, oldPath)
|
|
}
|
|
sort.Slice(oldPaths, func(i, j int) bool {
|
|
return strings.Count(oldPaths[i], string(os.PathSeparator)) < strings.Count(oldPaths[j], string(os.PathSeparator))
|
|
})
|
|
for i, oldPath := range oldPaths {
|
|
newPath := renamePaths[oldPath]
|
|
if err = filelock.Rename(oldPath, newPath); err != nil {
|
|
logging.LogErrorf("rename path from [%s] to [%s] failed: %s", oldPath, renamePaths[oldPath], err)
|
|
return errors.New("rename path failed")
|
|
}
|
|
|
|
delete(renamePaths, oldPath)
|
|
var toRemoves []string
|
|
newRenamedPaths := map[string]string{}
|
|
for oldP, newP := range renamePaths {
|
|
if strings.HasPrefix(oldP, oldPath) {
|
|
renamedOldP := strings.Replace(oldP, oldPath, newPath, 1)
|
|
newRenamedPaths[renamedOldP] = newP
|
|
toRemoves = append(toRemoves, oldPath)
|
|
}
|
|
}
|
|
for _, toRemove := range toRemoves {
|
|
delete(renamePaths, toRemove)
|
|
}
|
|
for oldP, newP := range newRenamedPaths {
|
|
renamePaths[oldP] = newP
|
|
}
|
|
for j := i + 1; j < len(oldPaths); j++ {
|
|
if strings.HasPrefix(oldPaths[j], oldPath) {
|
|
renamedOldP := strings.Replace(oldPaths[j], oldPath, newPath, 1)
|
|
oldPaths[j] = renamedOldP
|
|
}
|
|
}
|
|
}
|
|
|
|
// 将包含的资源文件统一移动到 data/assets/ 下
|
|
var assetsDirs []string
|
|
filelock.Walk(unzipRootPath, func(path string, d fs.DirEntry, err error) error {
|
|
if strings.Contains(path, "assets") && d.IsDir() {
|
|
assetsDirs = append(assetsDirs, path)
|
|
}
|
|
return nil
|
|
})
|
|
dataAssets := filepath.Join(util.DataDir, "assets")
|
|
for _, assets := range assetsDirs {
|
|
if gulu.File.IsDir(assets) {
|
|
if err = filelock.Copy(assets, dataAssets); err != nil {
|
|
logging.LogErrorf("copy assets from [%s] to [%s] failed: %s", assets, dataAssets, err)
|
|
return
|
|
}
|
|
}
|
|
os.RemoveAll(assets)
|
|
}
|
|
|
|
var baseTargetPath string
|
|
if "/" == toPath {
|
|
baseTargetPath = "/"
|
|
} else {
|
|
block := treenode.GetBlockTreeRootByPath(boxID, toPath)
|
|
if nil == block {
|
|
logging.LogErrorf("not found block by path [%s]", toPath)
|
|
return nil
|
|
}
|
|
baseTargetPath = strings.TrimSuffix(block.Path, ".sy")
|
|
}
|
|
|
|
targetDir := filepath.Join(util.DataDir, boxID, baseTargetPath)
|
|
if err = os.MkdirAll(targetDir, 0755); err != nil {
|
|
return
|
|
}
|
|
|
|
var treePaths []string
|
|
filelock.Walk(unzipRootPath, func(path string, d fs.DirEntry, err error) error {
|
|
if d.IsDir() {
|
|
if strings.HasPrefix(d.Name(), ".") {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if !strings.HasSuffix(d.Name(), ".sy") {
|
|
return nil
|
|
}
|
|
|
|
p := strings.TrimPrefix(path, unzipRootPath)
|
|
p = filepath.ToSlash(p)
|
|
treePaths = append(treePaths, p)
|
|
return nil
|
|
})
|
|
|
|
if err = filelock.Copy(unzipRootPath, targetDir); err != nil {
|
|
logging.LogErrorf("copy data dir from [%s] to [%s] failed: %s", unzipRootPath, util.DataDir, err)
|
|
err = errors.New("copy data failed")
|
|
return
|
|
}
|
|
|
|
boxAbsPath := filepath.Join(util.DataDir, boxID)
|
|
for _, treePath := range treePaths {
|
|
absPath := filepath.Join(targetDir, treePath)
|
|
p := strings.TrimPrefix(absPath, boxAbsPath)
|
|
p = filepath.ToSlash(p)
|
|
tree, err := filesys.LoadTree(boxID, p, luteEngine)
|
|
if err != nil {
|
|
logging.LogErrorf("load tree [%s] failed: %s", treePath, err)
|
|
continue
|
|
}
|
|
|
|
treenode.IndexBlockTree(tree)
|
|
sql.IndexTreeQueue(tree)
|
|
util.PushEndlessProgress(Conf.language(73) + " " + fmt.Sprintf(Conf.language(70), tree.Root.IALAttr("title")))
|
|
}
|
|
|
|
IncSync()
|
|
|
|
task.AppendTask(task.UpdateIDs, util.PushUpdateIDs, blockIDs)
|
|
return
|
|
}
|
|
|
|
func ImportData(zipPath string) (err error) {
|
|
util.PushEndlessProgress(Conf.Language(73))
|
|
defer util.ClearPushProgress(100)
|
|
|
|
lockSync()
|
|
defer unlockSync()
|
|
|
|
logging.LogInfof("import data from [%s]", zipPath)
|
|
baseName := filepath.Base(zipPath)
|
|
ext := filepath.Ext(baseName)
|
|
baseName = strings.TrimSuffix(baseName, ext)
|
|
unzipPath := filepath.Join(filepath.Dir(zipPath), baseName)
|
|
err = gulu.Zip.Unzip(zipPath, unzipPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer os.RemoveAll(unzipPath)
|
|
|
|
files, err := filepath.Glob(filepath.Join(unzipPath, "*/*.sy"))
|
|
if err != nil {
|
|
logging.LogErrorf("check data.zip failed: %s", err)
|
|
return errors.New("check data.zip failed")
|
|
}
|
|
if 0 < len(files) {
|
|
return errors.New(Conf.Language(198))
|
|
}
|
|
dirs, err := os.ReadDir(unzipPath)
|
|
if err != nil {
|
|
logging.LogErrorf("check data.zip failed: %s", err)
|
|
return errors.New("check data.zip failed")
|
|
}
|
|
if 1 != len(dirs) {
|
|
return errors.New(Conf.Language(198))
|
|
}
|
|
|
|
tmpDataPath := filepath.Join(unzipPath, dirs[0].Name())
|
|
if err = filelock.Copy(tmpDataPath, util.DataDir); err != nil {
|
|
logging.LogErrorf("copy data dir from [%s] to [%s] failed: %s", tmpDataPath, util.DataDir, err)
|
|
err = errors.New("copy data failed")
|
|
return
|
|
}
|
|
|
|
logging.LogInfof("import data from [%s] done", zipPath)
|
|
IncSync()
|
|
FullReindex()
|
|
return
|
|
}
|
|
|
|
func ImportFromLocalPath(boxID, localPath string, toPath string) (err error) {
|
|
util.PushEndlessProgress(Conf.Language(73))
|
|
defer func() {
|
|
util.PushClearProgress()
|
|
|
|
if e := recover(); nil != e {
|
|
stack := debug.Stack()
|
|
msg := fmt.Sprintf("PANIC RECOVERED: %v\n\t%s\n", e, stack)
|
|
logging.LogErrorf("import from local path failed: %s", msg)
|
|
err = errors.New("import from local path failed, please check kernel log for details")
|
|
}
|
|
}()
|
|
|
|
lockSync()
|
|
defer unlockSync()
|
|
|
|
FlushTxQueue()
|
|
|
|
var baseHPath, baseTargetPath, boxLocalPath string
|
|
if "/" == toPath {
|
|
baseHPath = "/"
|
|
baseTargetPath = "/"
|
|
} else {
|
|
block := treenode.GetBlockTreeRootByPath(boxID, toPath)
|
|
if nil == block {
|
|
logging.LogErrorf("not found block by path [%s]", toPath)
|
|
return nil
|
|
}
|
|
baseHPath = block.HPath
|
|
baseTargetPath = strings.TrimSuffix(block.Path, ".sy")
|
|
}
|
|
boxLocalPath = filepath.Join(util.DataDir, boxID)
|
|
|
|
hPathsIDs := map[string]string{}
|
|
idPaths := map[string]string{}
|
|
moveIDs := map[string]string{}
|
|
|
|
if gulu.File.IsDir(localPath) { // 导入文件夹
|
|
// 收集所有资源文件
|
|
assets := map[string]string{}
|
|
filelock.Walk(localPath, func(currentPath string, d fs.DirEntry, err error) error {
|
|
if localPath == currentPath {
|
|
return nil
|
|
}
|
|
if strings.HasPrefix(d.Name(), ".") {
|
|
if d.IsDir() {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if !strings.HasSuffix(d.Name(), ".md") && !strings.HasSuffix(d.Name(), ".markdown") {
|
|
assets[currentPath] = currentPath
|
|
return nil
|
|
}
|
|
return nil
|
|
})
|
|
|
|
targetPaths := map[string]string{}
|
|
assetsDone := map[string]string{}
|
|
count := 0
|
|
// md 转换 sy
|
|
filelock.Walk(localPath, func(currentPath string, d fs.DirEntry, err error) error {
|
|
if strings.HasPrefix(d.Name(), ".") {
|
|
if d.IsDir() {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var tree *parse.Tree
|
|
var ext string
|
|
title := d.Name()
|
|
if !d.IsDir() {
|
|
ext = util.Ext(d.Name())
|
|
title = strings.TrimSuffix(d.Name(), ext)
|
|
}
|
|
id := ast.NewNodeID()
|
|
|
|
curRelPath := filepath.ToSlash(strings.TrimPrefix(currentPath, localPath))
|
|
targetPath := path.Join(baseTargetPath, id)
|
|
hPath := path.Join(baseHPath, filepath.Base(localPath), filepath.ToSlash(strings.TrimPrefix(currentPath, localPath)))
|
|
hPath = strings.TrimSuffix(hPath, ext)
|
|
if "" == curRelPath {
|
|
curRelPath = "/"
|
|
hPath = "/" + title
|
|
} else {
|
|
dirPath := targetPaths[path.Dir(curRelPath)]
|
|
targetPath = path.Join(dirPath, id)
|
|
}
|
|
|
|
targetPath = strings.ReplaceAll(targetPath, ".sy/", "/")
|
|
targetPath += ".sy"
|
|
if _, ok := targetPaths[curRelPath]; !ok {
|
|
targetPaths[curRelPath] = targetPath
|
|
} else {
|
|
targetPath = targetPaths[curRelPath]
|
|
id = util.GetTreeID(targetPath)
|
|
}
|
|
|
|
if d.IsDir() {
|
|
if subMdFiles := util.GetFilePathsByExts(currentPath, []string{".md", ".markdown"}); 1 > len(subMdFiles) {
|
|
// 如果该文件夹中不包含 Markdown 文件则不处理 https://github.com/siyuan-note/siyuan/issues/11567
|
|
return nil
|
|
}
|
|
|
|
// 如果当前文件夹路径下包含同名的 Markdown 文件,则不创建空文档 https://github.com/siyuan-note/siyuan/issues/13149
|
|
if gulu.File.IsExist(currentPath+".md") || gulu.File.IsExist(currentPath+".markdown") {
|
|
targetPaths[curRelPath+".md"] = targetPath
|
|
return nil
|
|
}
|
|
|
|
tree = treenode.NewTree(boxID, targetPath, hPath, title)
|
|
importTrees = append(importTrees, tree)
|
|
return nil
|
|
}
|
|
|
|
if !strings.HasSuffix(d.Name(), ".md") && !strings.HasSuffix(d.Name(), ".markdown") {
|
|
return nil
|
|
}
|
|
|
|
data, readErr := os.ReadFile(currentPath)
|
|
if nil != readErr {
|
|
err = readErr
|
|
return io.EOF
|
|
}
|
|
|
|
tree, yfmRootID, yfmTitle, yfmUpdated := parseStdMd(data)
|
|
if nil == tree {
|
|
logging.LogErrorf("parse tree [%s] failed", currentPath)
|
|
return nil
|
|
}
|
|
|
|
if "" != yfmRootID {
|
|
moveIDs[id] = yfmRootID
|
|
id = yfmRootID
|
|
}
|
|
if "" != yfmTitle {
|
|
title = yfmTitle
|
|
}
|
|
unescapedTitle, unescapeErr := url.PathUnescape(title)
|
|
if nil == unescapeErr {
|
|
title = unescapedTitle
|
|
}
|
|
hPath = path.Join(path.Dir(hPath), title)
|
|
updated := yfmUpdated
|
|
fname := path.Base(targetPath)
|
|
targetPath = strings.ReplaceAll(targetPath, fname, id+".sy")
|
|
targetPaths[curRelPath] = targetPath
|
|
|
|
tree.ID = id
|
|
tree.Root.ID = id
|
|
tree.Root.SetIALAttr("id", tree.Root.ID)
|
|
tree.Root.SetIALAttr("title", title)
|
|
tree.Box = boxID
|
|
targetPath = path.Join(path.Dir(targetPath), tree.Root.ID+".sy")
|
|
tree.Path = targetPath
|
|
targetPaths[curRelPath] = targetPath
|
|
tree.HPath = hPath
|
|
tree.Root.Spec = "1"
|
|
|
|
docDirLocalPath := filepath.Dir(filepath.Join(boxLocalPath, targetPath))
|
|
assetDirPath := getAssetsDir(boxLocalPath, docDirLocalPath)
|
|
currentDir := filepath.Dir(currentPath)
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering || (ast.NodeLinkDest != n.Type && !n.IsTextMarkType("a")) {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
var dest string
|
|
if ast.NodeLinkDest == n.Type {
|
|
dest = n.TokensStr()
|
|
} else {
|
|
dest = n.TextMarkAHref
|
|
}
|
|
|
|
if strings.HasPrefix(dest, "data:image") && strings.Contains(dest, ";base64,") {
|
|
processBase64Img(n, dest, assetDirPath)
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
dest = strings.ReplaceAll(dest, "%20", " ")
|
|
dest = strings.ReplaceAll(dest, "%5C", "/")
|
|
if ast.NodeLinkDest == n.Type {
|
|
n.Tokens = []byte(dest)
|
|
} else {
|
|
n.TextMarkAHref = dest
|
|
}
|
|
if !util.IsRelativePath(dest) {
|
|
return ast.WalkContinue
|
|
}
|
|
dest = filepath.ToSlash(dest)
|
|
if "" == dest {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
absDest := filepath.Join(currentDir, dest)
|
|
fullPath, exist := assets[absDest]
|
|
if !exist {
|
|
absDest = filepath.Join(currentDir, string(html.DecodeDestination([]byte(dest))))
|
|
}
|
|
fullPath, exist = assets[absDest]
|
|
if exist {
|
|
existName := assetsDone[absDest]
|
|
var name string
|
|
if "" == existName {
|
|
name = filepath.Base(fullPath)
|
|
name = util.AssetName(name)
|
|
assetTargetPath := filepath.Join(assetDirPath, name)
|
|
if err = filelock.Copy(fullPath, assetTargetPath); err != nil {
|
|
logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", fullPath, assetTargetPath, err)
|
|
return ast.WalkContinue
|
|
}
|
|
assetsDone[absDest] = name
|
|
} else {
|
|
name = existName
|
|
}
|
|
if ast.NodeLinkDest == n.Type {
|
|
n.Tokens = []byte("assets/" + name)
|
|
} else {
|
|
n.TextMarkAHref = "assets/" + name
|
|
}
|
|
}
|
|
return ast.WalkContinue
|
|
})
|
|
|
|
reassignIDUpdated(tree, id, updated)
|
|
importTrees = append(importTrees, tree)
|
|
|
|
hPathsIDs[tree.HPath] = tree.ID
|
|
idPaths[tree.ID] = tree.Path
|
|
|
|
count++
|
|
if 0 == count%4 {
|
|
util.PushEndlessProgress(fmt.Sprintf(Conf.language(70), fmt.Sprintf("%s", tree.HPath)))
|
|
}
|
|
return nil
|
|
})
|
|
} else { // 导入单个文件
|
|
fileName := filepath.Base(localPath)
|
|
if !strings.HasSuffix(fileName, ".md") && !strings.HasSuffix(fileName, ".markdown") {
|
|
return errors.New(Conf.Language(79))
|
|
}
|
|
|
|
title := strings.TrimSuffix(fileName, ".markdown")
|
|
title = strings.TrimSuffix(title, ".md")
|
|
targetPath := strings.TrimSuffix(toPath, ".sy")
|
|
id := ast.NewNodeID()
|
|
targetPath = path.Join(targetPath, id+".sy")
|
|
var data []byte
|
|
data, err = os.ReadFile(localPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tree, yfmRootID, yfmTitle, yfmUpdated := parseStdMd(data)
|
|
if nil == tree {
|
|
msg := fmt.Sprintf("parse tree [%s] failed", localPath)
|
|
logging.LogErrorf(msg)
|
|
return errors.New(msg)
|
|
}
|
|
|
|
if "" != yfmRootID {
|
|
id = yfmRootID
|
|
}
|
|
if "" != yfmTitle {
|
|
title = yfmTitle
|
|
}
|
|
unescapedTitle, unescapeErr := url.PathUnescape(title)
|
|
if nil == unescapeErr {
|
|
title = unescapedTitle
|
|
}
|
|
updated := yfmUpdated
|
|
fname := path.Base(targetPath)
|
|
targetPath = strings.ReplaceAll(targetPath, fname, id+".sy")
|
|
|
|
tree.ID = id
|
|
tree.Root.ID = id
|
|
tree.Root.SetIALAttr("id", tree.Root.ID)
|
|
tree.Root.SetIALAttr("title", title)
|
|
tree.Box = boxID
|
|
tree.Path = targetPath
|
|
tree.HPath = path.Join(baseHPath, title)
|
|
tree.Root.Spec = "1"
|
|
|
|
docDirLocalPath := filepath.Dir(filepath.Join(boxLocalPath, targetPath))
|
|
assetDirPath := getAssetsDir(boxLocalPath, docDirLocalPath)
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering || (ast.NodeLinkDest != n.Type && !n.IsTextMarkType("a")) {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
var dest string
|
|
if ast.NodeLinkDest == n.Type {
|
|
dest = n.TokensStr()
|
|
} else {
|
|
dest = n.TextMarkAHref
|
|
}
|
|
|
|
if strings.HasPrefix(dest, "data:image") && strings.Contains(dest, ";base64,") {
|
|
processBase64Img(n, dest, assetDirPath)
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
dest = strings.ReplaceAll(dest, "%20", " ")
|
|
dest = strings.ReplaceAll(dest, "%5C", "/")
|
|
if ast.NodeLinkDest == n.Type {
|
|
n.Tokens = []byte(dest)
|
|
} else {
|
|
n.TextMarkAHref = dest
|
|
}
|
|
if !util.IsRelativePath(dest) {
|
|
return ast.WalkContinue
|
|
}
|
|
dest = filepath.ToSlash(dest)
|
|
if "" == dest {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
absolutePath := filepath.Join(filepath.Dir(localPath), dest)
|
|
exist := gulu.File.IsExist(absolutePath)
|
|
if !exist {
|
|
absolutePath = filepath.Join(filepath.Dir(localPath), string(html.DecodeDestination([]byte(dest))))
|
|
exist = gulu.File.IsExist(absolutePath)
|
|
}
|
|
if exist {
|
|
name := filepath.Base(absolutePath)
|
|
name = util.AssetName(name)
|
|
assetTargetPath := filepath.Join(assetDirPath, name)
|
|
if err = filelock.Copy(absolutePath, assetTargetPath); err != nil {
|
|
logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", absolutePath, assetTargetPath, err)
|
|
return ast.WalkContinue
|
|
}
|
|
if ast.NodeLinkDest == n.Type {
|
|
n.Tokens = []byte("assets/" + name)
|
|
} else {
|
|
n.TextMarkAHref = "assets/" + name
|
|
}
|
|
}
|
|
return ast.WalkContinue
|
|
})
|
|
|
|
reassignIDUpdated(tree, id, updated)
|
|
importTrees = append(importTrees, tree)
|
|
}
|
|
|
|
if 0 < len(importTrees) {
|
|
for id, newID := range moveIDs {
|
|
for _, importTree := range importTrees {
|
|
importTree.ID = strings.ReplaceAll(importTree.ID, id, newID)
|
|
importTree.Path = strings.ReplaceAll(importTree.Path, id, newID)
|
|
}
|
|
}
|
|
|
|
initSearchLinks()
|
|
convertWikiLinksAndTags()
|
|
buildBlockRefInText()
|
|
|
|
box := Conf.Box(boxID)
|
|
for i, tree := range importTrees {
|
|
indexWriteTreeIndexQueue(tree)
|
|
if 0 == i%4 {
|
|
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(66), fmt.Sprintf("%d/%d ", i, len(importTrees))+tree.HPath))
|
|
}
|
|
}
|
|
util.PushClearProgress()
|
|
|
|
importTrees = []*parse.Tree{}
|
|
searchLinks = map[string]string{}
|
|
|
|
// 按照路径排序 Improve sort when importing markdown files https://github.com/siyuan-note/siyuan/issues/11390
|
|
var hPaths []string
|
|
for hPath := range hPathsIDs {
|
|
hPaths = append(hPaths, hPath)
|
|
}
|
|
sort.Strings(hPaths)
|
|
paths := map[string][]string{}
|
|
for _, hPath := range hPaths {
|
|
p := idPaths[hPathsIDs[hPath]]
|
|
parent := path.Dir(p)
|
|
for {
|
|
if baseTargetPath == parent {
|
|
break
|
|
}
|
|
|
|
if ps, ok := paths[parent]; !ok {
|
|
paths[parent] = []string{p}
|
|
} else {
|
|
ps = append(ps, p)
|
|
ps = gulu.Str.RemoveDuplicatedElem(ps)
|
|
paths[parent] = ps
|
|
}
|
|
p = parent
|
|
parent = path.Dir(parent)
|
|
}
|
|
}
|
|
|
|
sortIDVals := map[string]int{}
|
|
for _, ps := range paths {
|
|
sortVal := 0
|
|
for _, p := range ps {
|
|
sortIDVals[util.GetTreeID(p)] = sortVal
|
|
sortVal++
|
|
}
|
|
}
|
|
box.setSort(sortIDVals)
|
|
}
|
|
|
|
IncSync()
|
|
debug.FreeOSMemory()
|
|
return
|
|
}
|
|
|
|
func parseStdMd(markdown []byte) (ret *parse.Tree, yfmRootID, yfmTitle, yfmUpdated string) {
|
|
luteEngine := util.NewStdLute()
|
|
luteEngine.SetYamlFrontMatter(true) // 解析 YAML Front Matter https://github.com/siyuan-note/siyuan/issues/10878
|
|
ret = parse.Parse("", markdown, luteEngine.ParseOptions)
|
|
if nil == ret {
|
|
return
|
|
}
|
|
yfmRootID, yfmTitle, yfmUpdated = normalizeTree(ret)
|
|
imgHtmlBlock2InlineImg(ret)
|
|
parse.TextMarks2Inlines(ret) // 先将 TextMark 转换为 Inlines https://github.com/siyuan-note/siyuan/issues/13056
|
|
parse.NestedInlines2FlattedSpansHybrid(ret, false)
|
|
return
|
|
}
|
|
|
|
func processBase64Img(n *ast.Node, dest string, assetDirPath string) {
|
|
base64TmpDir := filepath.Join(util.TempDir, "base64")
|
|
os.MkdirAll(base64TmpDir, 0755)
|
|
|
|
sep := strings.Index(dest, ";base64,")
|
|
str := strings.TrimSpace(dest[sep+8:])
|
|
re := regexp.MustCompile(`(?i)%0A`)
|
|
str = re.ReplaceAllString(str, "\n")
|
|
var decodeErr error
|
|
unbased, decodeErr := base64.StdEncoding.DecodeString(str)
|
|
if nil != decodeErr {
|
|
logging.LogErrorf("decode base64 image failed: %s", decodeErr)
|
|
return
|
|
}
|
|
dataReader := bytes.NewReader(unbased)
|
|
var img image.Image
|
|
var ext string
|
|
typ := dest[5:sep]
|
|
switch typ {
|
|
case "image/png":
|
|
img, decodeErr = png.Decode(dataReader)
|
|
ext = ".png"
|
|
case "image/jpeg":
|
|
img, decodeErr = jpeg.Decode(dataReader)
|
|
ext = ".jpg"
|
|
default:
|
|
logging.LogWarnf("unsupported base64 image type [%s]", typ)
|
|
return
|
|
}
|
|
if nil != decodeErr {
|
|
logging.LogErrorf("decode base64 image failed: %s", decodeErr)
|
|
return
|
|
}
|
|
|
|
name := "image" + ext
|
|
alt := n.Parent.ChildByType(ast.NodeLinkText)
|
|
if nil != alt {
|
|
name = alt.TokensStr() + ext
|
|
}
|
|
name = util.FilterUploadFileName(name)
|
|
name = util.AssetName(name)
|
|
|
|
tmp := filepath.Join(base64TmpDir, name)
|
|
tmpFile, openErr := os.OpenFile(tmp, os.O_RDWR|os.O_CREATE, 0644)
|
|
if nil != openErr {
|
|
logging.LogErrorf("open temp file [%s] failed: %s", tmp, openErr)
|
|
return
|
|
}
|
|
|
|
var encodeErr error
|
|
switch typ {
|
|
case "image/png":
|
|
encodeErr = png.Encode(tmpFile, img)
|
|
case "image/jpeg":
|
|
encodeErr = jpeg.Encode(tmpFile, img, &jpeg.Options{Quality: 100})
|
|
}
|
|
if nil != encodeErr {
|
|
logging.LogErrorf("encode base64 image failed: %s", encodeErr)
|
|
tmpFile.Close()
|
|
return
|
|
}
|
|
tmpFile.Close()
|
|
|
|
assetTargetPath := filepath.Join(assetDirPath, name)
|
|
if err := filelock.Copy(tmp, assetTargetPath); err != nil {
|
|
logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", tmp, assetTargetPath, err)
|
|
return
|
|
}
|
|
n.Tokens = []byte("assets/" + name)
|
|
}
|
|
|
|
func imgHtmlBlock2InlineImg(tree *parse.Tree) {
|
|
imgHtmlBlocks := map[*ast.Node]*html.Node{}
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
if ast.NodeHTMLBlock == n.Type || (ast.NodeText == n.Type && bytes.HasPrefix(bytes.ToLower(n.Tokens), []byte("<img "))) {
|
|
tokens := bytes.TrimSpace(n.Tokens)
|
|
if bytes.HasPrefix(tokens, []byte("<div>")) {
|
|
tokens = bytes.TrimPrefix(tokens, []byte("<div>"))
|
|
}
|
|
if bytes.HasSuffix(tokens, []byte("</div>")) {
|
|
tokens = bytes.TrimSuffix(tokens, []byte("</div>"))
|
|
}
|
|
tokens = bytes.TrimSpace(tokens)
|
|
|
|
htmlNodes, pErr := html.ParseFragment(bytes.NewReader(tokens), &html.Node{Type: html.ElementNode})
|
|
if nil != pErr {
|
|
logging.LogErrorf("parse html block [%s] failed: %s", n.Tokens, pErr)
|
|
return ast.WalkContinue
|
|
}
|
|
if 1 > len(htmlNodes) {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
for _, htmlNode := range htmlNodes {
|
|
if atom.Img == htmlNode.DataAtom {
|
|
imgHtmlBlocks[n] = htmlNode
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return ast.WalkContinue
|
|
})
|
|
|
|
for n, htmlImg := range imgHtmlBlocks {
|
|
src := domAttrValue(htmlImg, "src")
|
|
alt := domAttrValue(htmlImg, "alt")
|
|
title := domAttrValue(htmlImg, "title")
|
|
|
|
p := treenode.NewParagraph(n.ID)
|
|
img := &ast.Node{Type: ast.NodeImage}
|
|
p.AppendChild(img)
|
|
img.AppendChild(&ast.Node{Type: ast.NodeBang})
|
|
img.AppendChild(&ast.Node{Type: ast.NodeOpenBracket})
|
|
img.AppendChild(&ast.Node{Type: ast.NodeLinkText, Tokens: []byte(alt)})
|
|
img.AppendChild(&ast.Node{Type: ast.NodeCloseBracket})
|
|
img.AppendChild(&ast.Node{Type: ast.NodeOpenParen})
|
|
img.AppendChild(&ast.Node{Type: ast.NodeLinkDest, Tokens: []byte(src)})
|
|
if "" != title {
|
|
img.AppendChild(&ast.Node{Type: ast.NodeLinkSpace})
|
|
img.AppendChild(&ast.Node{Type: ast.NodeLinkTitle})
|
|
}
|
|
img.AppendChild(&ast.Node{Type: ast.NodeCloseParen})
|
|
|
|
if nil != n.Parent && ast.NodeText == n.Type {
|
|
// 行级 HTML 会被解析为文本,所以这里要在父级段落前面插入,避免形成段落嵌套 https://github.com/siyuan-note/siyuan/issues/13080
|
|
n.Parent.InsertBefore(p)
|
|
} else {
|
|
n.InsertBefore(p)
|
|
}
|
|
n.Unlink()
|
|
}
|
|
return
|
|
}
|
|
|
|
func reassignIDUpdated(tree *parse.Tree, rootID, updated string) {
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering || "" == n.ID {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
n.ID = ast.NewNodeID()
|
|
if ast.NodeDocument == n.Type && "" != rootID {
|
|
n.ID = rootID
|
|
}
|
|
|
|
n.SetIALAttr("id", n.ID)
|
|
if "" != updated {
|
|
n.SetIALAttr("updated", updated)
|
|
if "" == rootID {
|
|
n.ID = updated + "-" + gulu.Rand.String(7)
|
|
n.SetIALAttr("id", n.ID)
|
|
}
|
|
} else {
|
|
n.SetIALAttr("updated", util.TimeFromID(n.ID))
|
|
}
|
|
return ast.WalkContinue
|
|
})
|
|
tree.ID = tree.Root.ID
|
|
tree.Path = path.Join(path.Dir(tree.Path), tree.ID+".sy")
|
|
tree.Root.SetIALAttr("id", tree.Root.ID)
|
|
}
|
|
|
|
func domAttrValue(n *html.Node, attrName string) string {
|
|
if nil == n {
|
|
return ""
|
|
}
|
|
|
|
for _, attr := range n.Attr {
|
|
if attr.Key == attrName {
|
|
return attr.Val
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
var importTrees []*parse.Tree
|
|
var searchLinks = map[string]string{}
|
|
|
|
func initSearchLinks() {
|
|
for _, tree := range importTrees {
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering || (ast.NodeDocument != n.Type && ast.NodeHeading != n.Type) {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
nodePath := tree.HPath + "#"
|
|
if ast.NodeHeading == n.Type {
|
|
nodePath += n.Text()
|
|
}
|
|
|
|
searchLinks[nodePath] = n.ID
|
|
return ast.WalkContinue
|
|
})
|
|
}
|
|
}
|
|
|
|
func convertWikiLinksAndTags() {
|
|
for _, tree := range importTrees {
|
|
convertWikiLinksAndTags0(tree)
|
|
}
|
|
}
|
|
|
|
func convertWikiLinksAndTags0(tree *parse.Tree) {
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering || ast.NodeText != n.Type {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
text := n.TokensStr()
|
|
length := len(text)
|
|
start, end := 0, length
|
|
for {
|
|
part := text[start:end]
|
|
if idx := strings.Index(part, "]]"); 0 > idx {
|
|
break
|
|
} else {
|
|
end = start + idx
|
|
}
|
|
if idx := strings.Index(part, "[["); 0 > idx {
|
|
break
|
|
} else {
|
|
start += idx
|
|
}
|
|
if end <= start {
|
|
break
|
|
}
|
|
|
|
link := path.Join(path.Dir(tree.HPath), text[start+2:end]) // 统一转为绝对路径方便后续查找
|
|
linkText := path.Base(link)
|
|
dynamicAnchorText := true
|
|
if linkParts := strings.Split(link, "|"); 1 < len(linkParts) {
|
|
link = linkParts[0]
|
|
linkText = linkParts[1]
|
|
dynamicAnchorText = false
|
|
}
|
|
link, linkText = strings.TrimSpace(link), strings.TrimSpace(linkText)
|
|
if !strings.Contains(link, "#") {
|
|
link += "#" // 在结尾统一带上锚点方便后续查找
|
|
}
|
|
|
|
id := searchLinkID(link)
|
|
if "" == id {
|
|
start, end = end, length
|
|
continue
|
|
}
|
|
|
|
linkText = strings.TrimPrefix(linkText, "/")
|
|
repl := "((" + id + " '" + linkText + "'))"
|
|
if !dynamicAnchorText {
|
|
repl = "((" + id + " \"" + linkText + "\"))"
|
|
}
|
|
end += 2
|
|
text = text[:start] + repl + text[end:]
|
|
start, end = start+len(repl), len(text)
|
|
length = end
|
|
}
|
|
|
|
text = convertTags(text) // 导入标签语法
|
|
n.Tokens = gulu.Str.ToBytes(text)
|
|
return ast.WalkContinue
|
|
})
|
|
}
|
|
|
|
func convertTags(text string) (ret string) {
|
|
if !util.MarkdownSettings.InlineTag {
|
|
return text
|
|
}
|
|
|
|
pos, i := -1, 0
|
|
tokens := []byte(text)
|
|
for ; i < len(tokens); i++ {
|
|
if '#' == tokens[i] && (0 == i || ' ' == tokens[i-1] || (-1 < pos && '#' == tokens[pos])) {
|
|
if i < len(tokens)-1 && '#' == tokens[i+1] {
|
|
pos = -1
|
|
continue
|
|
}
|
|
pos = i
|
|
continue
|
|
}
|
|
|
|
if -1 < pos && ' ' == tokens[i] {
|
|
tokens = append(tokens, 0)
|
|
copy(tokens[i+1:], tokens[i:])
|
|
tokens[i] = '#'
|
|
pos = -1
|
|
i++
|
|
}
|
|
}
|
|
if -1 < pos && pos < i {
|
|
tokens = append(tokens, '#')
|
|
}
|
|
return string(tokens)
|
|
}
|
|
|
|
// buildBlockRefInText 将文本节点进行结构化处理。
|
|
func buildBlockRefInText() {
|
|
luteEngine := NewLute()
|
|
luteEngine.SetHTMLTag2TextMark(true)
|
|
for _, tree := range importTrees {
|
|
tree.MergeText()
|
|
|
|
var unlinkTextNodes []*ast.Node
|
|
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
|
if !entering || ast.NodeText != n.Type {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
if nil == n.Tokens {
|
|
return ast.WalkContinue
|
|
}
|
|
|
|
t := parse.Inline("", n.Tokens, luteEngine.ParseOptions) // 使用行级解析
|
|
parse.NestedInlines2FlattedSpans(t, false)
|
|
var children []*ast.Node
|
|
for c := t.Root.FirstChild.FirstChild; nil != c; c = c.Next {
|
|
children = append(children, c)
|
|
}
|
|
for _, c := range children {
|
|
n.InsertBefore(c)
|
|
}
|
|
unlinkTextNodes = append(unlinkTextNodes, n)
|
|
return ast.WalkContinue
|
|
})
|
|
|
|
for _, node := range unlinkTextNodes {
|
|
node.Unlink()
|
|
}
|
|
}
|
|
}
|
|
|
|
func searchLinkID(link string) (id string) {
|
|
id = searchLinks[link]
|
|
if "" != id {
|
|
return
|
|
}
|
|
|
|
baseName := path.Base(link)
|
|
for searchLink, searchID := range searchLinks {
|
|
if path.Base(searchLink) == baseName {
|
|
return searchID
|
|
}
|
|
}
|
|
return
|
|
}
|