From 3e0d82543700ac638db1b4e73577f6d8701cd5f0 Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Sat, 26 Jul 2025 16:11:52 +0800 Subject: [PATCH 1/7] :art: Database grouping by field https://github.com/siyuan-note/siyuan/issues/10964 --- kernel/model/attribute_view.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kernel/model/attribute_view.go b/kernel/model/attribute_view.go index 43e00e2ba..5d2538b97 100644 --- a/kernel/model/attribute_view.go +++ b/kernel/model/attribute_view.go @@ -3838,6 +3838,10 @@ func updateAttributeViewColumn(operation *Operation) (err error) { } } + for _, view := range attrView.Views { + removeAttributeViewGroup0(view) + } + err = av.SaveAttributeView(attrView) return } From 7476372054ff12575569d77bd6b4bd67339750db Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Sat, 26 Jul 2025 16:42:50 +0800 Subject: [PATCH 2/7] :art: Database grouping by field https://github.com/siyuan-note/siyuan/issues/10964 --- kernel/model/attribute_view.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/kernel/model/attribute_view.go b/kernel/model/attribute_view.go index 5d2538b97..b62f46c09 100644 --- a/kernel/model/attribute_view.go +++ b/kernel/model/attribute_view.go @@ -3820,6 +3820,7 @@ func updateAttributeViewColumn(operation *Operation) (err error) { } colType := av.KeyType(operation.Typ) + changeType := false switch colType { 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, @@ -3827,6 +3828,8 @@ func updateAttributeViewColumn(operation *Operation) (err error) { for _, keyValues := range attrView.KeyValues { if keyValues.Key.ID == operation.ID { keyValues.Key.Name = strings.TrimSpace(operation.Name) + + changeType = keyValues.Key.Type != colType keyValues.Key.Type = colType for _, value := range keyValues.Values { @@ -3838,8 +3841,10 @@ func updateAttributeViewColumn(operation *Operation) (err error) { } } - for _, view := range attrView.Views { - removeAttributeViewGroup0(view) + if changeType { + for _, view := range attrView.Views { + removeAttributeViewGroup0(view) + } } err = av.SaveAttributeView(attrView) From 9d392bd663a04ebe33b44a375f11b017ee9522c5 Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Sat, 26 Jul 2025 16:50:46 +0800 Subject: [PATCH 3/7] :art: Improve asset search highlighting https://github.com/siyuan-note/siyuan/issues/15370 --- kernel/model/assets.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kernel/model/assets.go b/kernel/model/assets.go index 91acf052f..9769985c1 100644 --- a/kernel/model/assets.go +++ b/kernel/model/assets.go @@ -343,7 +343,9 @@ func SearchAssetsByName(keyword string, exts []string) (ret []*cache.Asset) { ret = []*cache.Asset{} var keywords []string keywords = append(keywords, keyword) - keywords = append(keywords, strings.Split(keyword, " ")...) + if "" != keyword { + keywords = append(keywords, strings.Split(keyword, " ")...) + } pathHitCount := map[string]int{} filterByExt := 0 < len(exts) for _, asset := range cache.GetAssets() { From 7c692e82739bce730cc1b63a126f25c355b4ce95 Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Sat, 26 Jul 2025 21:24:28 +0800 Subject: [PATCH 4/7] :zap: Improve pasting performance for large amounts of content https://github.com/siyuan-note/siyuan/issues/15306 --- app/src/protyle/util/insertHTML.ts | 12 +- app/src/protyle/util/paste.ts | 2 +- kernel/model/file.go | 5 + kernel/model/transaction.go | 530 +++++++++++++++++++---------- 4 files changed, 362 insertions(+), 187 deletions(-) diff --git a/app/src/protyle/util/insertHTML.ts b/app/src/protyle/util/insertHTML.ts index fc64cf4b3..9a4a3a2a9 100644 --- a/app/src/protyle/util/insertHTML.ts +++ b/app/src/protyle/util/insertHTML.ts @@ -4,10 +4,12 @@ import {transaction, updateTransaction} from "../wysiwyg/transaction"; import {getContenteditableElement} from "../wysiwyg/getBlock"; import { fixTableRange, - focusBlock, focusByRange, + focusBlock, + focusByRange, focusByWbr, getEditorRange, - getSelectionOffset, setLastNodeRange, + getSelectionOffset, + setLastNodeRange, } from "./selection"; import {Constants} from "../../constants"; import {highlightRender} from "../render/highlightRender"; @@ -257,7 +259,9 @@ export const insertHTML = (html: string, protyle: IProtyle, isBlock = false, // 移动端插入嵌入块时,获取到的 range 为旧值 useProtyleRange = false, // 在开头粘贴块则插入上方 - insertByCursor = false) => { + insertByCursor = false, + // 是否需要再次 spin + spin = true) => { if (html === "") { return; } @@ -364,7 +368,7 @@ export const insertHTML = (html: string, protyle: IProtyle, isBlock = false, } 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 + (spin && protyle.lute.SpinBlockDOM(html)) || // 需要再 spin 一次 https://github.com/siyuan-note/siyuan/issues/7118 html; // 空格会被 Spin 不再,需要使用原文 // 粘贴纯文本时会进行内部转义,这里需要进行反转义 https://github.com/siyuan-note/siyuan/issues/10620 innerHTML = innerHTML.replace(/;;;lt;;;/g, "<").replace(/;;;gt;;;/g, ">"); diff --git a/app/src/protyle/util/paste.ts b/app/src/protyle/util/paste.ts index 26fc192b5..10181aed6 100644 --- a/app/src/protyle/util/paste.ts +++ b/app/src/protyle/util/paste.ts @@ -591,7 +591,7 @@ export const paste = async (protyle: IProtyle, event: (ClipboardEvent | DragEven textPlain = textPlain.replace(/\u200D```/g, "```"); const textPlainDom = protyle.lute.Md2BlockDOM(textPlain); - insertHTML(textPlainDom, protyle, false, false, true); + insertHTML(textPlainDom, protyle, false, false, true, false /* 不再次 spin 以提升性能 https://github.com/siyuan-note/siyuan/issues/15306 */); } blockRender(protyle, protyle.wysiwyg.element); processRender(protyle.wysiwyg.element); diff --git a/kernel/model/file.go b/kernel/model/file.go index 2cd5cf816..654f46ee2 100644 --- a/kernel/model/file.go +++ b/kernel/model/file.go @@ -764,6 +764,11 @@ func loadNodesByStartEnd(tree *parse.Tree, startID, endID string) (nodes []*ast. } break } + + if len(nodes) >= Conf.Editor.DynamicLoadBlocks { + // 如果加载到指定数量的块则停止加载 + break + } } return } diff --git a/kernel/model/transaction.go b/kernel/model/transaction.go index a9aee1db6..9f1867ca3 100644 --- a/kernel/model/transaction.go +++ b/kernel/model/transaction.go @@ -167,153 +167,156 @@ func performTx(tx *Transaction) (ret *TxErr) { } }() - for _, op := range tx.DoOperations { - switch op.Action { - case "create": - ret = tx.doCreate(op) - case "update": - ret = tx.doUpdate(op) - case "insert": - ret = tx.doInsert(op) - case "delete": - ret = tx.doDelete(op) - case "move": - ret = tx.doMove(op) - case "moveOutlineHeading": - ret = tx.doMoveOutlineHeading(op) - case "append": - ret = tx.doAppend(op) - case "appendInsert": - ret = tx.doAppendInsert(op) - case "prependInsert": - ret = tx.doPrependInsert(op) - case "foldHeading": - ret = tx.doFoldHeading(op) - case "unfoldHeading": - ret = tx.doUnfoldHeading(op) - case "setAttrs": - ret = tx.doSetAttrs(op) - case "doUpdateUpdated": - ret = tx.doUpdateUpdated(op) - case "addFlashcards": - ret = tx.doAddFlashcards(op) - case "removeFlashcards": - ret = tx.doRemoveFlashcards(op) - case "setAttrViewName": - ret = tx.doSetAttrViewName(op) - case "setAttrViewFilters": - ret = tx.doSetAttrViewFilters(op) - case "setAttrViewSorts": - ret = tx.doSetAttrViewSorts(op) - case "setAttrViewPageSize": - ret = tx.doSetAttrViewPageSize(op) - case "setAttrViewColWidth": - ret = tx.doSetAttrViewColumnWidth(op) - case "setAttrViewColWrap": - ret = tx.doSetAttrViewColumnWrap(op) - case "setAttrViewColHidden": - ret = tx.doSetAttrViewColumnHidden(op) - case "setAttrViewColPin": - ret = tx.doSetAttrViewColumnPin(op) - case "setAttrViewColIcon": - ret = tx.doSetAttrViewColumnIcon(op) - case "setAttrViewColDesc": - ret = tx.doSetAttrViewColumnDesc(op) - case "insertAttrViewBlock": - ret = tx.doInsertAttrViewBlock(op) - case "removeAttrViewBlock": - ret = tx.doRemoveAttrViewBlock(op) - case "addAttrViewCol": - ret = tx.doAddAttrViewColumn(op) - case "updateAttrViewCol": - ret = tx.doUpdateAttrViewColumn(op) - case "removeAttrViewCol": - ret = tx.doRemoveAttrViewColumn(op) - case "sortAttrViewRow": - ret = tx.doSortAttrViewRow(op) - case "sortAttrViewCol": - ret = tx.doSortAttrViewColumn(op) - case "sortAttrViewKey": - ret = tx.doSortAttrViewKey(op) - case "updateAttrViewCell": - ret = tx.doUpdateAttrViewCell(op) - case "updateAttrViewColOptions": - ret = tx.doUpdateAttrViewColOptions(op) - case "removeAttrViewColOption": - ret = tx.doRemoveAttrViewColOption(op) - case "updateAttrViewColOption": - ret = tx.doUpdateAttrViewColOption(op) - case "setAttrViewColOptionDesc": - ret = tx.doSetAttrViewColOptionDesc(op) - case "setAttrViewColCalc": - ret = tx.doSetAttrViewColCalc(op) - case "updateAttrViewColNumberFormat": - ret = tx.doUpdateAttrViewColNumberFormat(op) - case "replaceAttrViewBlock": - ret = tx.doReplaceAttrViewBlock(op) - case "updateAttrViewColTemplate": - ret = tx.doUpdateAttrViewColTemplate(op) - case "addAttrViewView": - ret = tx.doAddAttrViewView(op) - case "removeAttrViewView": - ret = tx.doRemoveAttrViewView(op) - case "setAttrViewViewName": - ret = tx.doSetAttrViewViewName(op) - case "setAttrViewViewIcon": - ret = tx.doSetAttrViewViewIcon(op) - case "setAttrViewViewDesc": - ret = tx.doSetAttrViewViewDesc(op) - case "duplicateAttrViewView": - ret = tx.doDuplicateAttrViewView(op) - case "sortAttrViewView": - ret = tx.doSortAttrViewView(op) - case "updateAttrViewColRelation": - ret = tx.doUpdateAttrViewColRelation(op) - case "updateAttrViewColRollup": - ret = tx.doUpdateAttrViewColRollup(op) - case "hideAttrViewName": - ret = tx.doHideAttrViewName(op) - case "setAttrViewColDate": - ret = tx.doSetAttrViewColDate(op) - case "unbindAttrViewBlock": - ret = tx.doUnbindAttrViewBlock(op) - case "duplicateAttrViewKey": - ret = tx.doDuplicateAttrViewKey(op) - case "setAttrViewCoverFrom": - ret = tx.doSetAttrViewCoverFrom(op) - case "setAttrViewCoverFromAssetKeyID": - ret = tx.doSetAttrViewCoverFromAssetKeyID(op) - case "setAttrViewCardSize": - ret = tx.doSetAttrViewCardSize(op) - case "setAttrViewFitImage": - ret = tx.doSetAttrViewFitImage(op) - case "setAttrViewShowIcon": - ret = tx.doSetAttrViewShowIcon(op) - case "setAttrViewWrapField": - ret = tx.doSetAttrViewWrapField(op) - case "changeAttrViewLayout": - ret = tx.doChangeAttrViewLayout(op) - case "setAttrViewBlockView": - ret = tx.doSetAttrViewBlockView(op) - case "setAttrViewCardAspectRatio": - ret = tx.doSetAttrViewCardAspectRatio(op) - case "setAttrViewGroup": - ret = tx.doSetAttrViewGroup(op) - case "hideAttrViewGroup": - ret = tx.doHideAttrViewGroup(op) - case "foldAttrViewGroup": - ret = tx.doFoldAttrViewGroup(op) - case "syncAttrViewTableColWidth": - ret = tx.doSyncAttrViewTableColWidth(op) - case "removeAttrViewGroup": - ret = tx.doRemoveAttrViewGroup(op) - case "sortAttrViewGroup": - ret = tx.doSortAttrViewGroup(op) - } + isLargeInsert := tx.processLargeInsert() + if !isLargeInsert { + for _, op := range tx.DoOperations { + switch op.Action { + case "create": + ret = tx.doCreate(op) + case "update": + ret = tx.doUpdate(op) + case "insert": + ret = tx.doInsert(op) + case "delete": + ret = tx.doDelete(op) + case "move": + ret = tx.doMove(op) + case "moveOutlineHeading": + ret = tx.doMoveOutlineHeading(op) + case "append": + ret = tx.doAppend(op) + case "appendInsert": + ret = tx.doAppendInsert(op) + case "prependInsert": + ret = tx.doPrependInsert(op) + case "foldHeading": + ret = tx.doFoldHeading(op) + case "unfoldHeading": + ret = tx.doUnfoldHeading(op) + case "setAttrs": + ret = tx.doSetAttrs(op) + case "doUpdateUpdated": + ret = tx.doUpdateUpdated(op) + case "addFlashcards": + ret = tx.doAddFlashcards(op) + case "removeFlashcards": + ret = tx.doRemoveFlashcards(op) + case "setAttrViewName": + ret = tx.doSetAttrViewName(op) + case "setAttrViewFilters": + ret = tx.doSetAttrViewFilters(op) + case "setAttrViewSorts": + ret = tx.doSetAttrViewSorts(op) + case "setAttrViewPageSize": + ret = tx.doSetAttrViewPageSize(op) + case "setAttrViewColWidth": + ret = tx.doSetAttrViewColumnWidth(op) + case "setAttrViewColWrap": + ret = tx.doSetAttrViewColumnWrap(op) + case "setAttrViewColHidden": + ret = tx.doSetAttrViewColumnHidden(op) + case "setAttrViewColPin": + ret = tx.doSetAttrViewColumnPin(op) + case "setAttrViewColIcon": + ret = tx.doSetAttrViewColumnIcon(op) + case "setAttrViewColDesc": + ret = tx.doSetAttrViewColumnDesc(op) + case "insertAttrViewBlock": + ret = tx.doInsertAttrViewBlock(op) + case "removeAttrViewBlock": + ret = tx.doRemoveAttrViewBlock(op) + case "addAttrViewCol": + ret = tx.doAddAttrViewColumn(op) + case "updateAttrViewCol": + ret = tx.doUpdateAttrViewColumn(op) + case "removeAttrViewCol": + ret = tx.doRemoveAttrViewColumn(op) + case "sortAttrViewRow": + ret = tx.doSortAttrViewRow(op) + case "sortAttrViewCol": + ret = tx.doSortAttrViewColumn(op) + case "sortAttrViewKey": + ret = tx.doSortAttrViewKey(op) + case "updateAttrViewCell": + ret = tx.doUpdateAttrViewCell(op) + case "updateAttrViewColOptions": + ret = tx.doUpdateAttrViewColOptions(op) + case "removeAttrViewColOption": + ret = tx.doRemoveAttrViewColOption(op) + case "updateAttrViewColOption": + ret = tx.doUpdateAttrViewColOption(op) + case "setAttrViewColOptionDesc": + ret = tx.doSetAttrViewColOptionDesc(op) + case "setAttrViewColCalc": + ret = tx.doSetAttrViewColCalc(op) + case "updateAttrViewColNumberFormat": + ret = tx.doUpdateAttrViewColNumberFormat(op) + case "replaceAttrViewBlock": + ret = tx.doReplaceAttrViewBlock(op) + case "updateAttrViewColTemplate": + ret = tx.doUpdateAttrViewColTemplate(op) + case "addAttrViewView": + ret = tx.doAddAttrViewView(op) + case "removeAttrViewView": + ret = tx.doRemoveAttrViewView(op) + case "setAttrViewViewName": + ret = tx.doSetAttrViewViewName(op) + case "setAttrViewViewIcon": + ret = tx.doSetAttrViewViewIcon(op) + case "setAttrViewViewDesc": + ret = tx.doSetAttrViewViewDesc(op) + case "duplicateAttrViewView": + ret = tx.doDuplicateAttrViewView(op) + case "sortAttrViewView": + ret = tx.doSortAttrViewView(op) + case "updateAttrViewColRelation": + ret = tx.doUpdateAttrViewColRelation(op) + case "updateAttrViewColRollup": + ret = tx.doUpdateAttrViewColRollup(op) + case "hideAttrViewName": + ret = tx.doHideAttrViewName(op) + case "setAttrViewColDate": + ret = tx.doSetAttrViewColDate(op) + case "unbindAttrViewBlock": + ret = tx.doUnbindAttrViewBlock(op) + case "duplicateAttrViewKey": + ret = tx.doDuplicateAttrViewKey(op) + case "setAttrViewCoverFrom": + ret = tx.doSetAttrViewCoverFrom(op) + case "setAttrViewCoverFromAssetKeyID": + ret = tx.doSetAttrViewCoverFromAssetKeyID(op) + case "setAttrViewCardSize": + ret = tx.doSetAttrViewCardSize(op) + case "setAttrViewFitImage": + ret = tx.doSetAttrViewFitImage(op) + case "setAttrViewShowIcon": + ret = tx.doSetAttrViewShowIcon(op) + case "setAttrViewWrapField": + ret = tx.doSetAttrViewWrapField(op) + case "changeAttrViewLayout": + ret = tx.doChangeAttrViewLayout(op) + case "setAttrViewBlockView": + ret = tx.doSetAttrViewBlockView(op) + case "setAttrViewCardAspectRatio": + ret = tx.doSetAttrViewCardAspectRatio(op) + case "setAttrViewGroup": + ret = tx.doSetAttrViewGroup(op) + case "hideAttrViewGroup": + ret = tx.doHideAttrViewGroup(op) + case "foldAttrViewGroup": + ret = tx.doFoldAttrViewGroup(op) + case "syncAttrViewTableColWidth": + ret = tx.doSyncAttrViewTableColWidth(op) + case "removeAttrViewGroup": + ret = tx.doRemoveAttrViewGroup(op) + case "sortAttrViewGroup": + ret = tx.doSortAttrViewGroup(op) + } - if nil != ret { - tx.rollback() - return + if nil != ret { + tx.rollback() + return + } } } @@ -324,6 +327,42 @@ func performTx(tx *Transaction) (ret *TxErr) { 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) { var err error id := operation.ID @@ -980,6 +1019,127 @@ func syncDelete2AttributeView(node *ast.Node) (changedAvIDs []string) { 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) { var bt *treenode.BlockTree bts := treenode.GetBlockTrees([]string{operation.ParentID, operation.PreviousID, operation.NextID}) @@ -1005,42 +1165,7 @@ func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) { data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "") subTree := tx.luteEngine.BlockDOM2Tree(data) - - 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 - }) - } + tx.processGlobalAssets(subTree) insertedNode := subTree.Root.FirstChild if nil == insertedNode { @@ -1189,6 +1314,47 @@ func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) { 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) { id := operation.ID tree, err := tx.loadTree(id) From d555eec852c75dfa13a7089b43d6b2dae94f4695 Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Sat, 26 Jul 2025 21:35:17 +0800 Subject: [PATCH 5/7] :zap: Improve pasting performance for large amounts of content https://github.com/siyuan-note/siyuan/issues/15306 --- app/src/protyle/util/insertHTML.ts | 5 +---- app/src/protyle/util/paste.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/protyle/util/insertHTML.ts b/app/src/protyle/util/insertHTML.ts index 9a4a3a2a9..c5962d4b3 100644 --- a/app/src/protyle/util/insertHTML.ts +++ b/app/src/protyle/util/insertHTML.ts @@ -259,9 +259,7 @@ export const insertHTML = (html: string, protyle: IProtyle, isBlock = false, // 移动端插入嵌入块时,获取到的 range 为旧值 useProtyleRange = false, // 在开头粘贴块则插入上方 - insertByCursor = false, - // 是否需要再次 spin - spin = true) => { + insertByCursor = false) => { if (html === "") { return; } @@ -368,7 +366,6 @@ export const insertHTML = (html: string, protyle: IProtyle, isBlock = false, } let innerHTML = unSpinHTML || // 在 table 中插入需要使用转换好的行内元素 https://github.com/siyuan-note/siyuan/issues/9358 - (spin && protyle.lute.SpinBlockDOM(html)) || // 需要再 spin 一次 https://github.com/siyuan-note/siyuan/issues/7118 html; // 空格会被 Spin 不再,需要使用原文 // 粘贴纯文本时会进行内部转义,这里需要进行反转义 https://github.com/siyuan-note/siyuan/issues/10620 innerHTML = innerHTML.replace(/;;;lt;;;/g, "<").replace(/;;;gt;;;/g, ">"); diff --git a/app/src/protyle/util/paste.ts b/app/src/protyle/util/paste.ts index 10181aed6..26fc192b5 100644 --- a/app/src/protyle/util/paste.ts +++ b/app/src/protyle/util/paste.ts @@ -591,7 +591,7 @@ export const paste = async (protyle: IProtyle, event: (ClipboardEvent | DragEven textPlain = textPlain.replace(/\u200D```/g, "```"); const textPlainDom = protyle.lute.Md2BlockDOM(textPlain); - insertHTML(textPlainDom, protyle, false, false, true, false /* 不再次 spin 以提升性能 https://github.com/siyuan-note/siyuan/issues/15306 */); + insertHTML(textPlainDom, protyle, false, false, true); } blockRender(protyle, protyle.wysiwyg.element); processRender(protyle.wysiwyg.element); From 3cb666d397095bb430ba762167ad5148571d2f6d Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Sat, 26 Jul 2025 21:39:02 +0800 Subject: [PATCH 6/7] :art: Clean code --- kernel/model/transaction.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/kernel/model/transaction.go b/kernel/model/transaction.go index 9f1867ca3..f609305db 100644 --- a/kernel/model/transaction.go +++ b/kernel/model/transaction.go @@ -823,8 +823,6 @@ func (tx *Transaction) doAppend(operation *Operation) (ret *TxErr) { } func (tx *Transaction) doDelete(operation *Operation) (ret *TxErr) { - // logging.LogInfof("commit delete [%+v]", operation) - var err error id := operation.ID tree, err := tx.loadTree(id) From cf6c90593061b1c3a841fd37d499d2a735133046 Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Sat, 26 Jul 2025 21:59:05 +0800 Subject: [PATCH 7/7] :art: Export preview mode supports focus use https://github.com/siyuan-note/siyuan/issues/15340 --- app/src/protyle/preview/index.ts | 3 ++- kernel/api/lute.go | 8 +++++++- kernel/model/export.go | 17 +++++++++++------ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/src/protyle/preview/index.ts b/app/src/protyle/preview/index.ts index 6abbdd3fe..bb742a8ca 100644 --- a/app/src/protyle/preview/index.ts +++ b/app/src/protyle/preview/index.ts @@ -277,8 +277,9 @@ export class Preview { this.processZHTable(copyElement); } else if (type === "yuque") { fetchPost("/api/lute/copyStdMarkdown", { - id: protyle.block.rootID, + id: protyle.block.id || protyle.options.blockId || protyle.block.parentID, assetsDestSpace2Underscore: true, + adjustHeadingLevel: true, }, (response) => { writeText(response.data); showMessage(`${window.siyuan.languages.pasteToYuque}`); diff --git a/kernel/api/lute.go b/kernel/api/lute.go index bafe2976d..423f98be9 100644 --- a/kernel/api/lute.go +++ b/kernel/api/lute.go @@ -48,7 +48,13 @@ func copyStdMarkdown(c *gin.Context) { if nil != arg["assetsDestSpace2Underscore"] { 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) { diff --git a/kernel/model/export.go b/kernel/model/export.go index d3f9492b0..abacbb090 100644 --- a/kernel/model/export.go +++ b/kernel/model/export.go @@ -268,7 +268,7 @@ func Export2Liandi(id string) (err error) { title := path.Base(tree.HPath) 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, "#", "#", "", "", @@ -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) if err != nil { 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) - return exportMarkdownContent0(tree, cloudAssetsBase, assetsDestSpace2Underscore, + return exportMarkdownContent0(id, tree, cloudAssetsBase, assetsDestSpace2Underscore, adjustHeadingLevel, ".md", Conf.Export.BlockRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode, Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker, Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight, @@ -1989,7 +1989,7 @@ func ExportMarkdownContent(id string, refMode, embedMode int, addYfm, fillCSSVar tree := prepareExportTree(bt) hPath = tree.HPath - exportedMd = exportMarkdownContent0(tree, "", false, + exportedMd = exportMarkdownContent0(id, tree, "", false, false, ".md", refMode, embedMode, Conf.Export.FileAnnotationRefMode, Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker, Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight, @@ -2008,7 +2008,7 @@ func exportMarkdownContent(id, ext string, exportRefMode int, defBlockIDs []stri return } isEmpty = nil == tree.Root.FirstChild.FirstChild - exportedMd = exportMarkdownContent0(tree, "", false, + exportedMd = exportMarkdownContent0(id, tree, "", false, false, ext, exportRefMode, Conf.Export.BlockEmbedMode, Conf.Export.FileAnnotationRefMode, Conf.Export.TagOpenMarker, Conf.Export.TagCloseMarker, Conf.Export.BlockRefTextLeft, Conf.Export.BlockRefTextRight, @@ -2021,7 +2021,7 @@ func exportMarkdownContent(id, ext string, exportRefMode int, defBlockIDs []stri 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, tagOpenMarker, tagCloseMarker string, blockRefTextLeft, blockRefTextRight 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, blockRefTextLeft, blockRefTextRight, addTitle, inlineMemo, 0 < len(defBlockIDs), singleFile, treeCache) + if adjustHeadingLv { + bt := treenode.GetBlockTree(id) + adjustHeadingLevel(bt, tree) + } + luteEngine := NewLute() luteEngine.SetFootnotes(true) luteEngine.SetKramdownIAL(false)