Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Vanessa 2025-07-26 22:50:44 +08:00
commit a9f5cac024
8 changed files with 389 additions and 196 deletions

View file

@ -277,8 +277,9 @@ export class Preview {
this.processZHTable(copyElement); this.processZHTable(copyElement);
} else if (type === "yuque") { } else if (type === "yuque") {
fetchPost("/api/lute/copyStdMarkdown", { fetchPost("/api/lute/copyStdMarkdown", {
id: protyle.block.rootID, id: protyle.block.id || protyle.options.blockId || protyle.block.parentID,
assetsDestSpace2Underscore: true, assetsDestSpace2Underscore: true,
adjustHeadingLevel: true,
}, (response) => { }, (response) => {
writeText(response.data); writeText(response.data);
showMessage(`${window.siyuan.languages.pasteToYuque}`); showMessage(`${window.siyuan.languages.pasteToYuque}`);

View file

@ -4,10 +4,12 @@ import {transaction, updateTransaction} from "../wysiwyg/transaction";
import {getContenteditableElement} from "../wysiwyg/getBlock"; import {getContenteditableElement} from "../wysiwyg/getBlock";
import { import {
fixTableRange, fixTableRange,
focusBlock, focusByRange, focusBlock,
focusByRange,
focusByWbr, focusByWbr,
getEditorRange, getEditorRange,
getSelectionOffset, setLastNodeRange, getSelectionOffset,
setLastNodeRange,
} from "./selection"; } from "./selection";
import {Constants} from "../../constants"; import {Constants} from "../../constants";
import {highlightRender} from "../render/highlightRender"; import {highlightRender} from "../render/highlightRender";
@ -364,7 +366,6 @@ export const insertHTML = (html: string, protyle: IProtyle, isBlock = false,
} }
let innerHTML = unSpinHTML || // 在 table 中插入需要使用转换好的行内元素 https://github.com/siyuan-note/siyuan/issues/9358 let innerHTML = unSpinHTML || // 在 table 中插入需要使用转换好的行内元素 https://github.com/siyuan-note/siyuan/issues/9358
protyle.lute.SpinBlockDOM(html) || // 需要再 spin 一次 https://github.com/siyuan-note/siyuan/issues/7118
html; // 空格会被 Spin 不再,需要使用原文 html; // 空格会被 Spin 不再,需要使用原文
// 粘贴纯文本时会进行内部转义,这里需要进行反转义 https://github.com/siyuan-note/siyuan/issues/10620 // 粘贴纯文本时会进行内部转义,这里需要进行反转义 https://github.com/siyuan-note/siyuan/issues/10620
innerHTML = innerHTML.replace(/;;;lt;;;/g, "<").replace(/;;;gt;;;/g, ">"); innerHTML = innerHTML.replace(/;;;lt;;;/g, "<").replace(/;;;gt;;;/g, ">");

View file

@ -48,7 +48,13 @@ func copyStdMarkdown(c *gin.Context) {
if nil != arg["assetsDestSpace2Underscore"] { if nil != arg["assetsDestSpace2Underscore"] {
assetsDestSpace2Underscore = arg["assetsDestSpace2Underscore"].(bool) assetsDestSpace2Underscore = arg["assetsDestSpace2Underscore"].(bool)
} }
ret.Data = model.ExportStdMarkdown(id, assetsDestSpace2Underscore)
adjustHeadingLevel := false
if nil != arg["adjustHeadingLevel"] {
adjustHeadingLevel = arg["adjustHeadingLevel"].(bool)
}
ret.Data = model.ExportStdMarkdown(id, assetsDestSpace2Underscore, adjustHeadingLevel)
} }
func html2BlockDOM(c *gin.Context) { func html2BlockDOM(c *gin.Context) {

View file

@ -343,7 +343,9 @@ func SearchAssetsByName(keyword string, exts []string) (ret []*cache.Asset) {
ret = []*cache.Asset{} ret = []*cache.Asset{}
var keywords []string var keywords []string
keywords = append(keywords, keyword) keywords = append(keywords, keyword)
if "" != keyword {
keywords = append(keywords, strings.Split(keyword, " ")...) keywords = append(keywords, strings.Split(keyword, " ")...)
}
pathHitCount := map[string]int{} pathHitCount := map[string]int{}
filterByExt := 0 < len(exts) filterByExt := 0 < len(exts)
for _, asset := range cache.GetAssets() { for _, asset := range cache.GetAssets() {

View file

@ -3820,6 +3820,7 @@ func updateAttributeViewColumn(operation *Operation) (err error) {
} }
colType := av.KeyType(operation.Typ) colType := av.KeyType(operation.Typ)
changeType := false
switch colType { switch colType {
case av.KeyTypeBlock, av.KeyTypeText, av.KeyTypeNumber, av.KeyTypeDate, av.KeyTypeSelect, av.KeyTypeMSelect, av.KeyTypeURL, av.KeyTypeEmail, case av.KeyTypeBlock, av.KeyTypeText, av.KeyTypeNumber, av.KeyTypeDate, av.KeyTypeSelect, av.KeyTypeMSelect, av.KeyTypeURL, av.KeyTypeEmail,
av.KeyTypePhone, av.KeyTypeMAsset, av.KeyTypeTemplate, av.KeyTypeCreated, av.KeyTypeUpdated, av.KeyTypeCheckbox, av.KeyTypePhone, av.KeyTypeMAsset, av.KeyTypeTemplate, av.KeyTypeCreated, av.KeyTypeUpdated, av.KeyTypeCheckbox,
@ -3827,6 +3828,8 @@ func updateAttributeViewColumn(operation *Operation) (err error) {
for _, keyValues := range attrView.KeyValues { for _, keyValues := range attrView.KeyValues {
if keyValues.Key.ID == operation.ID { if keyValues.Key.ID == operation.ID {
keyValues.Key.Name = strings.TrimSpace(operation.Name) keyValues.Key.Name = strings.TrimSpace(operation.Name)
changeType = keyValues.Key.Type != colType
keyValues.Key.Type = colType keyValues.Key.Type = colType
for _, value := range keyValues.Values { for _, value := range keyValues.Values {
@ -3838,6 +3841,12 @@ func updateAttributeViewColumn(operation *Operation) (err error) {
} }
} }
if changeType {
for _, view := range attrView.Views {
removeAttributeViewGroup0(view)
}
}
err = av.SaveAttributeView(attrView) err = av.SaveAttributeView(attrView)
return return
} }

View file

@ -268,7 +268,7 @@ func Export2Liandi(id string) (err error) {
title := path.Base(tree.HPath) title := path.Base(tree.HPath)
tags := tree.Root.IALAttr("tags") tags := tree.Root.IALAttr("tags")
content := exportMarkdownContent0(tree, util.GetCloudForumAssetsServer()+time.Now().Format("2006/01")+"/siyuan/"+Conf.GetUser().UserId+"/", true, content := exportMarkdownContent0(id, tree, util.GetCloudForumAssetsServer()+time.Now().Format("2006/01")+"/siyuan/"+Conf.GetUser().UserId+"/", true, false,
".md", 3, 1, 1, ".md", 3, 1, 1,
"#", "#", "#", "#",
"", "", "", "",
@ -1457,7 +1457,7 @@ func processPDFLinkEmbedAssets(pdfCtx *model.Context, assetDests []string, remov
} }
} }
func ExportStdMarkdown(id string, assetsDestSpace2Underscore bool) string { func ExportStdMarkdown(id string, assetsDestSpace2Underscore, adjustHeadingLevel bool) string {
tree, err := LoadTreeByBlockID(id) tree, err := LoadTreeByBlockID(id)
if err != nil { if err != nil {
logging.LogErrorf("load tree by block id [%s] failed: %s", id, err) logging.LogErrorf("load tree by block id [%s] failed: %s", id, err)
@ -1495,7 +1495,7 @@ func ExportStdMarkdown(id string, assetsDestSpace2Underscore bool) string {
} }
defBlockIDs = gulu.Str.RemoveDuplicatedElem(defBlockIDs) defBlockIDs = gulu.Str.RemoveDuplicatedElem(defBlockIDs)
return exportMarkdownContent0(tree, cloudAssetsBase, assetsDestSpace2Underscore, return exportMarkdownContent0(id, tree, cloudAssetsBase, assetsDestSpace2Underscore, adjustHeadingLevel,
".md", Conf.Export.BlockRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode, ".md", Conf.Export.BlockRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode,
Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker, Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker,
Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight, Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight,
@ -1989,7 +1989,7 @@ func ExportMarkdownContent(id string, refMode, embedMode int, addYfm, fillCSSVar
tree := prepareExportTree(bt) tree := prepareExportTree(bt)
hPath = tree.HPath hPath = tree.HPath
exportedMd = exportMarkdownContent0(tree, "", false, exportedMd = exportMarkdownContent0(id, tree, "", false, false,
".md", refMode, embedMode, Conf.Export.FileAnnotationRefMode, ".md", refMode, embedMode, Conf.Export.FileAnnotationRefMode,
Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker, Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker,
Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight, Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight,
@ -2008,7 +2008,7 @@ func exportMarkdownContent(id, ext string, exportRefMode int, defBlockIDs []stri
return return
} }
isEmpty = nil == tree.Root.FirstChild.FirstChild isEmpty = nil == tree.Root.FirstChild.FirstChild
exportedMd = exportMarkdownContent0(tree, "", false, exportedMd = exportMarkdownContent0(id, tree, "", false, false,
ext, exportRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode, ext, exportRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode,
Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker, Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker,
Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight, Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight,
@ -2021,7 +2021,7 @@ func exportMarkdownContent(id, ext string, exportRefMode int, defBlockIDs []stri
return return
} }
func exportMarkdownContent0(tree *parse.Tree, cloudAssetsBase string, assetsDestSpace2Underscore bool, func exportMarkdownContent0(id string, tree *parse.Tree, cloudAssetsBase string, assetsDestSpace2Underscore, adjustHeadingLv bool,
ext string, blockRefMode, blockEmbedMode, fileAnnotationRefMode int, ext string, blockRefMode, blockEmbedMode, fileAnnotationRefMode int,
tagOpenMarker, tagCloseMarker string, blockRefTextLeft, blockRefTextRight string, tagOpenMarker, tagCloseMarker string, blockRefTextLeft, blockRefTextRight string,
addTitle, inlineMemo bool, defBlockIDs []string, singleFile, fillCSSVar bool, treeCache *map[string]*parse.Tree) (ret string) { addTitle, inlineMemo bool, defBlockIDs []string, singleFile, fillCSSVar bool, treeCache *map[string]*parse.Tree) (ret string) {
@ -2030,6 +2030,11 @@ func exportMarkdownContent0(tree *parse.Tree, cloudAssetsBase string, assetsDest
tagOpenMarker, tagCloseMarker, tagOpenMarker, tagCloseMarker,
blockRefTextLeft, blockRefTextRight, blockRefTextLeft, blockRefTextRight,
addTitle, inlineMemo, 0 < len(defBlockIDs), singleFile, treeCache) addTitle, inlineMemo, 0 < len(defBlockIDs), singleFile, treeCache)
if adjustHeadingLv {
bt := treenode.GetBlockTree(id)
adjustHeadingLevel(bt, tree)
}
luteEngine := NewLute() luteEngine := NewLute()
luteEngine.SetFootnotes(true) luteEngine.SetFootnotes(true)
luteEngine.SetKramdownIAL(false) luteEngine.SetKramdownIAL(false)

View file

@ -764,6 +764,11 @@ func loadNodesByStartEnd(tree *parse.Tree, startID, endID string) (nodes []*ast.
} }
break break
} }
if len(nodes) >= Conf.Editor.DynamicLoadBlocks {
// 如果加载到指定数量的块则停止加载
break
}
} }
return return
} }

View file

@ -167,6 +167,8 @@ func performTx(tx *Transaction) (ret *TxErr) {
} }
}() }()
isLargeInsert := tx.processLargeInsert()
if !isLargeInsert {
for _, op := range tx.DoOperations { for _, op := range tx.DoOperations {
switch op.Action { switch op.Action {
case "create": case "create":
@ -316,6 +318,7 @@ func performTx(tx *Transaction) (ret *TxErr) {
return return
} }
} }
}
if cr := tx.commit(); nil != cr { if cr := tx.commit(); nil != cr {
logging.LogErrorf("commit tx failed: %s", cr) logging.LogErrorf("commit tx failed: %s", cr)
@ -324,6 +327,42 @@ func performTx(tx *Transaction) (ret *TxErr) {
return return
} }
func (tx *Transaction) processLargeInsert() bool {
opSize := len(tx.DoOperations)
isLargeInsert := 128 < opSize
if isLargeInsert {
var previousID string
for i, op := range tx.DoOperations {
if i == opSize-1 {
if "delete" != op.Action {
// 最后一个是 delete
isLargeInsert = false
}
break
}
if "insert" != op.Action {
isLargeInsert = false
break
}
if "" == op.PreviousID {
isLargeInsert = false
break
}
if "" == previousID {
previousID = op.PreviousID
} else if previousID != op.PreviousID {
isLargeInsert = false
break
}
}
if isLargeInsert {
tx.doLargeInsert(previousID)
}
}
return isLargeInsert
}
func (tx *Transaction) doMove(operation *Operation) (ret *TxErr) { func (tx *Transaction) doMove(operation *Operation) (ret *TxErr) {
var err error var err error
id := operation.ID id := operation.ID
@ -784,8 +823,6 @@ func (tx *Transaction) doAppend(operation *Operation) (ret *TxErr) {
} }
func (tx *Transaction) doDelete(operation *Operation) (ret *TxErr) { func (tx *Transaction) doDelete(operation *Operation) (ret *TxErr) {
// logging.LogInfof("commit delete [%+v]", operation)
var err error var err error
id := operation.ID id := operation.ID
tree, err := tx.loadTree(id) tree, err := tx.loadTree(id)
@ -980,6 +1017,127 @@ func syncDelete2AttributeView(node *ast.Node) (changedAvIDs []string) {
return return
} }
func (tx *Transaction) doLargeInsert(previousID string) (ret *TxErr) {
tree, err := tx.loadTree(previousID)
if nil != err {
logging.LogErrorf("load tree [%s] failed: %s", previousID, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: previousID}
}
for _, operation := range tx.DoOperations {
if "insert" != operation.Action {
break
}
data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
subTree := tx.luteEngine.BlockDOM2Tree(data)
tx.processGlobalAssets(subTree)
insertedNode := subTree.Root.FirstChild
if nil == insertedNode {
return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: tree.ID}
}
var remains []*ast.Node
for remain := insertedNode.Next; nil != remain; remain = remain.Next {
if ast.NodeKramdownBlockIAL != remain.Type {
if "" == remain.ID {
remain.ID = ast.NewNodeID()
remain.SetIALAttr("id", remain.ID)
}
remains = append(remains, remain)
}
}
if "" == insertedNode.ID {
insertedNode.ID = ast.NewNodeID()
insertedNode.SetIALAttr("id", insertedNode.ID)
}
node := treenode.GetNodeInTree(tree, previousID)
if nil == node {
logging.LogErrorf("get node [%s] in tree [%s] failed", previousID, tree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: previousID}
}
if ast.NodeHeading == node.Type && "1" == node.IALAttr("fold") {
children := treenode.HeadingChildren(node)
if l := len(children); 0 < l {
node = children[l-1]
}
}
if ast.NodeList == insertedNode.Type && nil != node.Parent && ast.NodeList == node.Parent.Type {
insertedNode = insertedNode.FirstChild
}
for i := len(remains) - 1; 0 <= i; i-- {
remain := remains[i]
node.InsertAfter(remain)
}
node.InsertAfter(insertedNode)
createdUpdated(insertedNode)
tx.nodes[insertedNode.ID] = insertedNode
tx.trees[tree.ID] = tree
// 收集引用的定义块 ID
refDefIDs := getRefDefIDs(insertedNode)
// 推送定义节点引用计数
for _, defID := range refDefIDs {
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, defID)
}
upsertAvBlockRel(insertedNode)
// 复制为副本时将该副本块插入到数据库中 https://github.com/siyuan-note/siyuan/issues/11959
avs := insertedNode.IALAttr(av.NodeAttrNameAvs)
for _, avID := range strings.Split(avs, ",") {
if !ast.IsNodeIDPattern(avID) {
continue
}
AddAttributeViewBlock(tx, []map[string]interface{}{{
"id": insertedNode.ID,
"isDetached": false,
}}, avID, "", previousID, false)
ReloadAttrView(avID)
}
if ast.NodeAttributeView == insertedNode.Type {
// 插入数据库块时需要重新绑定其中已经存在的块
// 比如剪切操作时,会先进行 delete 数据库解绑块,这里需要重新绑定 https://github.com/siyuan-note/siyuan/issues/13031
attrView, parseErr := av.ParseAttributeView(insertedNode.AttributeViewID)
if nil == parseErr {
trees, toBindNodes := tx.getAttrViewBoundNodes(attrView)
for _, toBindNode := range toBindNodes {
t := trees[toBindNode.ID]
bindBlockAv0(tx, insertedNode.AttributeViewID, toBindNode, t)
}
// 设置视图 https://github.com/siyuan-note/siyuan/issues/15279
v := attrView.GetView(attrView.ViewID)
if nil != v {
insertedNode.AttributeViewType = string(v.LayoutType)
attrs := parse.IAL2Map(insertedNode.KramdownIAL)
if "" == attrs[av.NodeAttrView] {
attrs[av.NodeAttrView] = v.ID
err = setNodeAttrs(insertedNode, tree, attrs)
if err != nil {
logging.LogWarnf("set node [%s] attrs failed: %s", operation.BlockID, err)
return
}
}
}
}
}
operation.ID = insertedNode.ID
operation.ParentID = insertedNode.Parent.ID
}
if err = tx.writeTree(tree); nil != err {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: tree.ID}
}
return tx.doDelete(tx.DoOperations[len(tx.DoOperations)-1])
}
func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) { func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) {
var bt *treenode.BlockTree var bt *treenode.BlockTree
bts := treenode.GetBlockTrees([]string{operation.ParentID, operation.PreviousID, operation.NextID}) bts := treenode.GetBlockTrees([]string{operation.ParentID, operation.PreviousID, operation.NextID})
@ -1005,42 +1163,7 @@ func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) {
data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "") data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
subTree := tx.luteEngine.BlockDOM2Tree(data) subTree := tx.luteEngine.BlockDOM2Tree(data)
tx.processGlobalAssets(subTree)
if !tx.isGlobalAssetsInit {
tx.assetsDir = getAssetsDir(filepath.Join(util.DataDir, bt.BoxID), filepath.Dir(filepath.Join(util.DataDir, bt.BoxID, bt.Path)))
tx.isGlobalAssets = strings.HasPrefix(tx.assetsDir, filepath.Join(util.DataDir, "assets"))
tx.isGlobalAssetsInit = true
}
if !tx.isGlobalAssets {
// 本地资源文件需要移动到用户手动建立的 assets 下 https://github.com/siyuan-note/siyuan/issues/2410
ast.Walk(subTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeLinkDest == n.Type && bytes.HasPrefix(n.Tokens, []byte("assets/")) {
assetP := gulu.Str.FromBytes(n.Tokens)
assetPath, e := GetAssetAbsPath(assetP)
if nil != e {
logging.LogErrorf("get path of asset [%s] failed: %s", assetP, err)
return ast.WalkContinue
}
if !strings.HasPrefix(assetPath, filepath.Join(util.DataDir, "assets")) {
// 非全局 assets 则跳过
return ast.WalkContinue
}
// 只有全局 assets 才移动到相对 assets
targetP := filepath.Join(tx.assetsDir, filepath.Base(assetPath))
if e = filelock.Rename(assetPath, targetP); err != nil {
logging.LogErrorf("copy path of asset from [%s] to [%s] failed: %s", assetPath, targetP, err)
return ast.WalkContinue
}
}
return ast.WalkContinue
})
}
insertedNode := subTree.Root.FirstChild insertedNode := subTree.Root.FirstChild
if nil == insertedNode { if nil == insertedNode {
@ -1189,6 +1312,47 @@ func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) {
return return
} }
func (tx *Transaction) processGlobalAssets(tree *parse.Tree) {
if !tx.isGlobalAssetsInit {
tx.assetsDir = getAssetsDir(filepath.Join(util.DataDir, tree.Box), filepath.Dir(filepath.Join(util.DataDir, tree.Box, tree.Path)))
tx.isGlobalAssets = strings.HasPrefix(tx.assetsDir, filepath.Join(util.DataDir, "assets"))
tx.isGlobalAssetsInit = true
}
if tx.isGlobalAssets {
return
}
// 本地资源文件需要移动到用户手动建立的 assets 下 https://github.com/siyuan-note/siyuan/issues/2410
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeLinkDest == n.Type && bytes.HasPrefix(n.Tokens, []byte("assets/")) {
assetP := gulu.Str.FromBytes(n.Tokens)
assetPath, e := GetAssetAbsPath(assetP)
if nil != e {
logging.LogErrorf("get path of asset [%s] failed: %s", assetP, e)
return ast.WalkContinue
}
if !strings.HasPrefix(assetPath, filepath.Join(util.DataDir, "assets")) {
// 非全局 assets 则跳过
return ast.WalkContinue
}
// 只有全局 assets 才移动到相对 assets
targetP := filepath.Join(tx.assetsDir, filepath.Base(assetPath))
if e = filelock.Rename(assetPath, targetP); e != nil {
logging.LogErrorf("copy path of asset from [%s] to [%s] failed: %s", assetPath, targetP, e)
return ast.WalkContinue
}
}
return ast.WalkContinue
})
}
func (tx *Transaction) doUpdate(operation *Operation) (ret *TxErr) { func (tx *Transaction) doUpdate(operation *Operation) (ret *TxErr) {
id := operation.ID id := operation.ID
tree, err := tx.loadTree(id) tree, err := tx.loadTree(id)