diff --git a/packages/core/src/docs/data-model/text-x/__tests__/compose.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/compose.spec.ts index 3db21548b20..0dce6499736 100644 --- a/packages/core/src/docs/data-model/text-x/__tests__/compose.spec.ts +++ b/packages/core/src/docs/data-model/text-x/__tests__/compose.spec.ts @@ -16,6 +16,7 @@ import type { TextXAction } from '../action-types'; import { describe, expect, it } from 'vitest'; +import { UpdateDocsAttributeType } from '../../../../shared'; import { BooleanNumber } from '../../../../types/enum/text-style'; import { TextXActionType } from '../action-types'; import { TextX } from '../text-x'; @@ -325,6 +326,7 @@ describe('compose test cases', () => { const expect_actions: TextXAction[] = [{ t: TextXActionType.RETAIN, + coverType: UpdateDocsAttributeType.COVER, body: { dataStream: '', textRuns: [{ diff --git a/packages/core/src/docs/data-model/text-x/__tests__/transform-paragraph.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/transform-paragraph.spec.ts index ca1f9567be9..7e52c092660 100644 --- a/packages/core/src/docs/data-model/text-x/__tests__/transform-paragraph.spec.ts +++ b/packages/core/src/docs/data-model/text-x/__tests__/transform-paragraph.spec.ts @@ -209,7 +209,7 @@ describe('transform paragraph in body', () => { { t: TextXActionType.RETAIN, len: 1, - coverType: UpdateDocsAttributeType.REPLACE, + coverType: UpdateDocsAttributeType.COVER, body: { dataStream: '', paragraphs: [{ @@ -237,7 +237,7 @@ describe('transform paragraph in body', () => { { t: TextXActionType.RETAIN, len: 1, - coverType: UpdateDocsAttributeType.REPLACE, + coverType: UpdateDocsAttributeType.COVER, body: { dataStream: '', paragraphs: [{ @@ -346,6 +346,9 @@ describe('transform paragraph in body', () => { spaceBelow: { v: 20, }, + spaceAbove: { + v: 30, + }, }, }], }, @@ -366,6 +369,9 @@ describe('transform paragraph in body', () => { spaceBelow: { v: 20, }, + spaceAbove: { + v: 30, + }, }, }], }, @@ -385,6 +391,9 @@ describe('transform paragraph in body', () => { spaceBelow: { v: 20, }, + spaceAbove: { + v: 30, + }, }, }], }, @@ -405,11 +414,6 @@ describe('transform paragraph in body', () => { const composedAction1 = TextX.compose(actionsA, TextX.transform(actionsB, actionsA, 'left')); const composedAction2 = TextX.compose(actionsB, TextX.transform(actionsA, actionsB, 'right')); - // console.log(JSON.stringify(actionsA, null, 2)); - // console.log(JSON.stringify(TextX.transform(actionsB, actionsA, 'left'), null, 2)); - // console.log(JSON.stringify(composedAction1, null, 2)); - // console.log(JSON.stringify(composedAction2, null, 2)); - const resultC = TextX.apply(doc3, composedAction1); const resultD = TextX.apply(doc4, composedAction2); diff --git a/packages/core/src/docs/data-model/text-x/__tests__/transform-textrun.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/transform-textrun.spec.ts new file mode 100644 index 00000000000..46eea210af0 --- /dev/null +++ b/packages/core/src/docs/data-model/text-x/__tests__/transform-textrun.spec.ts @@ -0,0 +1,147 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IDocumentBody } from '../../../../types/interfaces/i-document-data'; +import type { TextXAction } from '../action-types'; +import { describe, expect, it } from 'vitest'; +import { UpdateDocsAttributeType } from '../../../../shared'; +import { BooleanNumber } from '../../../../types/enum'; +import { TextXActionType } from '../action-types'; +import { TextX } from '../text-x'; + +function getDefaultDocWithTextRuns() { + const doc: IDocumentBody = { + dataStream: 'w\r\n', + textRuns: [ + { + st: 0, + ed: 1, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + }; + + return doc; +} + +// Test Retain + Retain with different coverType in transform textRun. +describe('transform textRun in body', () => { + it('should pass test when REPLACE + REPLACE', () => { + const actionsA: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + coverType: UpdateDocsAttributeType.REPLACE, + body: { + dataStream: '', + textRuns: [{ + st: 0, + ed: 1, + ts: { + it: BooleanNumber.TRUE, + fs: 10, + }, + }], + }, + }, + ]; + + const actionsB: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + coverType: UpdateDocsAttributeType.REPLACE, + body: { + dataStream: '', + textRuns: [{ + st: 0, + ed: 1, + ts: { + it: BooleanNumber.FALSE, + fs: 14, + }, + }], + }, + }, + ]; + + const expectedTransformedActionFalse: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + coverType: UpdateDocsAttributeType.REPLACE, + body: { + dataStream: '', + textRuns: [{ + st: 0, + ed: 1, + ts: { + it: BooleanNumber.TRUE, + fs: 10, + }, + }], + }, + }, + ]; + + const expectedTransformedActionTrue: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + coverType: UpdateDocsAttributeType.REPLACE, + body: { + dataStream: '', + textRuns: [{ + st: 0, + ed: 1, + ts: { + it: BooleanNumber.FALSE, + fs: 14, + }, + }], + }, + }, + ]; + + expect(TextX.transform(actionsB, actionsA, 'left')).toEqual(expectedTransformedActionTrue); + expect(TextX.transform(actionsB, actionsA, 'right')).toEqual(expectedTransformedActionFalse); + + const doc1 = getDefaultDocWithTextRuns(); + const doc2 = getDefaultDocWithTextRuns(); + const doc3 = getDefaultDocWithTextRuns(); + const doc4 = getDefaultDocWithTextRuns(); + + const resultA = TextX.apply(TextX.apply(doc1, actionsA), TextX.transform(actionsB, actionsA, 'left')); + const resultB = TextX.apply(TextX.apply(doc2, actionsB), TextX.transform(actionsA, actionsB, 'right')); + + const composedAction1 = TextX.compose(actionsA, TextX.transform(actionsB, actionsA, 'left')); + const composedAction2 = TextX.compose(actionsB, TextX.transform(actionsA, actionsB, 'right')); + + // console.log(JSON.stringify(TextX.transform(actionsB, actionsA, 'left'), null, 2)); + // console.log(JSON.stringify(composedAction1, null, 2)); + // console.log(JSON.stringify(composedAction2, null, 2)); + + const resultC = TextX.apply(doc3, composedAction1); + const resultD = TextX.apply(doc4, composedAction2); + + expect(resultA).toEqual(resultB); + expect(resultC).toEqual(resultD); + expect(resultA).toEqual(resultC); + expect(composedAction1).toEqual(composedAction2); + }); +}); diff --git a/packages/core/src/docs/data-model/text-x/__tests__/utils.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/utils.spec.ts index 789cf5d7f7b..d0a4e5fd13c 100644 --- a/packages/core/src/docs/data-model/text-x/__tests__/utils.spec.ts +++ b/packages/core/src/docs/data-model/text-x/__tests__/utils.spec.ts @@ -18,6 +18,7 @@ import type { IDocumentBody } from '../../../../types/interfaces/i-document-data import type { IRetainAction } from '../action-types'; import { describe, expect, it } from 'vitest'; import { BooleanNumber } from '../../../../types/enum/text-style'; +import { PresetListType } from '../../preset-list-type'; import { TextXActionType } from '../action-types'; import { composeBody, getBodySlice, isUselessRetainAction } from '../utils'; @@ -237,7 +238,7 @@ describe('test text-x utils', () => { }).toThrowError(); }); - it('test composeBody fn both width paragraphs', () => { + it('test composeBody both with paragraphs', () => { const thisBody: IDocumentBody = { dataStream: 'hello\nworld', paragraphs: [{ @@ -272,6 +273,62 @@ describe('test text-x utils', () => { }); }); + it('test composeBody both with paragraphs and one has bullet list', () => { + const thisBody: IDocumentBody = { + dataStream: '', + paragraphs: [{ + startIndex: 0, + paragraphStyle: { + lineSpacing: 2, + }, + bullet: { + listType: PresetListType.BULLET_LIST, + listId: 'testBullet', + nestingLevel: 0, + textStyle: { + fs: 15, + }, + }, + }], + }; + + const otherBody: IDocumentBody = { + dataStream: '', + paragraphs: [{ + startIndex: 0, + paragraphStyle: { + lineSpacing: 1, + spaceBelow: { + v: 20, + }, + }, + }], + }; + + const composedBody = composeBody(thisBody, otherBody); + + expect(composedBody).toEqual({ + dataStream: '', + paragraphs: [{ + startIndex: 0, + paragraphStyle: { + lineSpacing: 1, + spaceBelow: { + v: 20, + }, + }, + bullet: { + listType: PresetListType.BULLET_LIST, + listId: 'testBullet', + nestingLevel: 0, + textStyle: { + fs: 15, + }, + }, + }], + }); + }); + it('test composeBody fn both width paragraphs and from retain', () => { const thisBody: IDocumentBody = { dataStream: '', diff --git a/packages/core/src/docs/data-model/text-x/apply-utils/update-apply.ts b/packages/core/src/docs/data-model/text-x/apply-utils/update-apply.ts index f38c90715c2..3af6f6cabb5 100644 --- a/packages/core/src/docs/data-model/text-x/apply-utils/update-apply.ts +++ b/packages/core/src/docs/data-model/text-x/apply-utils/update-apply.ts @@ -101,19 +101,19 @@ function updateTextRuns( // eslint-disable-next-line max-lines-per-function, complexity export function coverTextRuns( updateDataTextRuns: ITextRun[], - removeTextRuns: ITextRun[], + originTextRuns: ITextRun[], coverType: UpdateDocsAttributeType ) { - if (removeTextRuns.length === 0) { + if (originTextRuns.length === 0) { return updateDataTextRuns; } updateDataTextRuns = Tools.deepClone(updateDataTextRuns); - removeTextRuns = Tools.deepClone(removeTextRuns); + originTextRuns = Tools.deepClone(originTextRuns); const newUpdateTextRuns: ITextRun[] = []; const updateLength = updateDataTextRuns.length; - const removeLength = removeTextRuns.length; + const removeLength = originTextRuns.length; let updateIndex = 0; let removeIndex = 0; let pending: Nullable = null; @@ -131,13 +131,13 @@ export function coverTextRuns( while (updateIndex < updateLength && removeIndex < removeLength) { const { st: updateSt, ed: updateEd, ts: updateStyle } = updateDataTextRuns[updateIndex]; - const { st: removeSt, ed: removeEd, ts: removeStyle, sId } = removeTextRuns[removeIndex]; + const { st: removeSt, ed: removeEd, ts: originStyle, sId } = originTextRuns[removeIndex]; let newTs; if (coverType === UpdateDocsAttributeType.COVER) { - newTs = { ...removeStyle, ...updateStyle }; + newTs = { ...originStyle, ...updateStyle }; } else { - newTs = { ...updateStyle, ...removeStyle }; + newTs = { ...updateStyle }; } if (updateEd < removeSt) { @@ -148,7 +148,7 @@ export function coverTextRuns( updateIndex++; } else if (removeEd < updateSt) { if (!pushPendingAndReturnStatus()) { - newUpdateTextRuns.push(removeTextRuns[removeIndex]); + newUpdateTextRuns.push(originTextRuns[removeIndex]); } removeIndex++; @@ -156,7 +156,7 @@ export function coverTextRuns( const newTextRun = { st: Math.min(updateSt, removeSt), ed: Math.max(updateSt, removeSt), - ts: updateSt < removeSt ? { ...updateStyle } : { ...removeStyle }, + ts: updateSt < removeSt ? { ...updateStyle } : { ...originStyle }, sId: updateSt < removeSt ? undefined : sId, }; @@ -173,8 +173,8 @@ export function coverTextRuns( if (updateEd < removeEd) { updateIndex++; - removeTextRuns[removeIndex].st = updateEd; - if (removeTextRuns[removeIndex].st === removeTextRuns[removeIndex].ed) { + originTextRuns[removeIndex].st = updateEd; + if (originTextRuns[removeIndex].st === originTextRuns[removeIndex].ed) { removeIndex++; } } else { @@ -188,7 +188,7 @@ export function coverTextRuns( const pendingTextRun = { st: Math.min(updateEd, removeEd), ed: Math.max(updateEd, removeEd), - ts: updateEd < removeEd ? { ...removeStyle } : { ...updateStyle }, + ts: updateEd < removeEd ? { ...originStyle } : { ...updateStyle }, sId: updateEd < removeEd ? sId : undefined, }; @@ -201,7 +201,7 @@ export function coverTextRuns( // If the last textRun is also disjoint, then the last textRun needs to be pushed in `newUpdateTextRun` const tempTopTextRun = newUpdateTextRuns[newUpdateTextRuns.length - 1]; const updateLastTextRun = updateDataTextRuns[updateLength - 1]; - const removeLastTextRun = removeTextRuns[removeLength - 1]; + const removeLastTextRun = originTextRuns[removeLength - 1]; if (tempTopTextRun && (tempTopTextRun.ed !== Math.max(updateLastTextRun.ed, removeLastTextRun.ed))) { if (updateLastTextRun.ed > removeLastTextRun.ed) { diff --git a/packages/core/src/docs/data-model/text-x/text-x.ts b/packages/core/src/docs/data-model/text-x/text-x.ts index 97f88e2a86e..64c7828ffeb 100644 --- a/packages/core/src/docs/data-model/text-x/text-x.ts +++ b/packages/core/src/docs/data-model/text-x/text-x.ts @@ -40,6 +40,7 @@ export class TextX { return textXApply(doc, actions); } + // eslint-disable-next-line complexity static compose(thisActions: TextXAction[], otherActions: TextXAction[]): TextXAction[] { const thisIter = new ActionIterator(thisActions); const otherIter = new ActionIterator(otherActions); @@ -69,10 +70,14 @@ export class TextX { if (thisAction.body == null && otherAction.body == null) { textX.push(thisAction.len !== Number.POSITIVE_INFINITY ? thisAction : otherAction); // or otherAction } else if (thisAction.body && otherAction.body) { + const coverType = thisAction.coverType === UpdateDocsAttributeType.REPLACE || otherAction.coverType === UpdateDocsAttributeType.REPLACE + ? UpdateDocsAttributeType.REPLACE + : UpdateDocsAttributeType.COVER; + textX.push({ ...thisAction, t: TextXActionType.RETAIN, - coverType: otherAction.coverType, + coverType, body: composeBody(thisAction.body, otherAction.body, otherAction.coverType), }); } else { diff --git a/packages/core/src/docs/data-model/text-x/transform-utils.ts b/packages/core/src/docs/data-model/text-x/transform-utils.ts index 7b06b37b4d7..967e52c6af2 100644 --- a/packages/core/src/docs/data-model/text-x/transform-utils.ts +++ b/packages/core/src/docs/data-model/text-x/transform-utils.ts @@ -33,6 +33,8 @@ enum TextXTransformType { function transformTextRuns( originTextRuns: ITextRun[], targetTextRuns: ITextRun[], + originCoverType: UpdateDocsAttributeType, + targetCoverType: UpdateDocsAttributeType, transformType: TextXTransformType ) { if (originTextRuns.length === 0) { @@ -66,15 +68,58 @@ function transformTextRuns( if (transformType === TextXTransformType.COVER) { newTs = { ...targetStyle }; - } else { - newTs = { ...targetStyle }; - if (originStyle) { + if (originCoverType === UpdateDocsAttributeType.COVER && targetCoverType === UpdateDocsAttributeType.REPLACE && originStyle) { const keys = Object.keys(originStyle); for (const key of keys) { - if (newTs[key as keyof ITextStyle]) { - delete newTs[key as keyof ITextStyle]; + if (newTs[key as keyof ITextStyle] === undefined) { + newTs[key as keyof ITextStyle] = originStyle[key as keyof ITextStyle] as any; + } + } + } + } else { + newTs = { ...targetStyle }; + + if (originCoverType === UpdateDocsAttributeType.REPLACE) { + if (targetCoverType === UpdateDocsAttributeType.REPLACE) { + // If they are all REPLACE types, the highest priority is retained when transforming. + newTs = { ...originStyle }; + } else { + // If the target action is COVER and has a low priority, then the origin field should not be cover. + if (targetStyle && originStyle) { + const keys = Object.keys(targetStyle); + + for (const key of keys) { + if (originStyle[key as keyof ITextStyle]) { + delete newTs[key as keyof ITextStyle]; + } + } + } + } + } else { + // If the origin is of type Cover and the target action is of type REPLACE + // and has a low priority, then all fields in origin should be copied to the target + if (targetCoverType === UpdateDocsAttributeType.REPLACE) { + if (originStyle) { + const keys = Object.keys(originStyle); + + for (const key of keys) { + if (originStyle[key as keyof ITextStyle]) { + newTs[key as keyof ITextStyle] = originStyle[key as keyof ITextStyle] as any; + } + } + } + } else { + // If they are all of the Cover type, then the fields in the Origin need to be deleted. + if (originStyle) { + const keys = Object.keys(originStyle); + + for (const key of keys) { + if (newTs[key as keyof ITextStyle]) { + delete newTs[key as keyof ITextStyle]; + } + } } } } @@ -189,9 +234,7 @@ function transformParagraph( }; if (targetParagraph.paragraphStyle) { - paragraph.paragraphStyle = { - ...targetParagraph.paragraphStyle, - }; + paragraph.paragraphStyle = Tools.deepClone(targetParagraph.paragraphStyle); if (originParagraph.paragraphStyle) { if (originCoverType === UpdateDocsAttributeType.REPLACE) { @@ -244,8 +287,8 @@ function transformParagraph( const keys = Object.keys(originParagraph.paragraphStyle); for (const key of keys) { - if (paragraph.paragraphStyle![key as keyof IParagraphStyle]) { - delete paragraph.paragraphStyle![key as keyof IParagraphStyle]; + if (paragraph.paragraphStyle[key as keyof IParagraphStyle]) { + delete paragraph.paragraphStyle[key as keyof IParagraphStyle]; } } } @@ -267,7 +310,7 @@ function transformParagraph( ? Tools.deepClone(targetParagraph.bullet) : Tools.deepClone(originParagraph.bullet); } else { - if (transformType === TextXTransformType.COVER) { + if (transformType === TextXTransformType.COVER && targetParagraph.bullet !== undefined) { paragraph.bullet = Tools.deepClone(targetParagraph.bullet); } } @@ -299,18 +342,16 @@ export function transformBody( dataStream: '', }; - const coverType = thisCoverType === UpdateDocsAttributeType.REPLACE || otherCoverType === UpdateDocsAttributeType.REPLACE - ? UpdateDocsAttributeType.REPLACE - : UpdateDocsAttributeType.COVER; + const coverType = otherCoverType; const { textRuns: thisTextRuns = [], paragraphs: thisParagraphs = [], customRanges: thisCustomRanges = [] } = thisBody; const { textRuns: otherTextRuns = [], paragraphs: otherParagraphs = [], customRanges: otherCustomRanges = [] } = otherBody; let textRuns: ITextRun[] = []; if (priority) { - textRuns = transformTextRuns(thisTextRuns, otherTextRuns, TextXTransformType.COVER_ONLY_NOT_EXISTED); + textRuns = transformTextRuns(thisTextRuns, otherTextRuns, thisCoverType, otherCoverType, TextXTransformType.COVER_ONLY_NOT_EXISTED); } else { - textRuns = transformTextRuns(thisTextRuns, otherTextRuns, TextXTransformType.COVER); + textRuns = transformTextRuns(thisTextRuns, otherTextRuns, thisCoverType, otherCoverType, TextXTransformType.COVER); } if (textRuns.length) { retBody.textRuns = textRuns; diff --git a/packages/core/src/docs/data-model/text-x/utils.ts b/packages/core/src/docs/data-model/text-x/utils.ts index d63a8a52b4d..60c72232ba1 100644 --- a/packages/core/src/docs/data-model/text-x/utils.ts +++ b/packages/core/src/docs/data-model/text-x/utils.ts @@ -346,6 +346,7 @@ export function composeBody( if (paragraphs.length) { retBody.paragraphs = paragraphs; } + return retBody; }