From b0194b830a8c5e99974e24ba5938e71a4738996c Mon Sep 17 00:00:00 2001 From: jocs Date: Tue, 22 Oct 2024 20:54:45 +0800 Subject: [PATCH] fix: transform with REPLACE type --- .../data-model/text-x/__tests__/apply.spec.ts | 166 +++++++++++++++++- .../text-x/__tests__/transform.spec.ts | 88 ++++++++++ .../core/src/docs/data-model/text-x/text-x.ts | 9 +- .../docs/data-model/text-x/transform-utils.ts | 41 +++-- .../commands/doc-paragraph-setting.command.ts | 4 +- 5 files changed, 281 insertions(+), 27 deletions(-) diff --git a/packages/core/src/docs/data-model/text-x/__tests__/apply.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/apply.spec.ts index b98dba4b107..0d23e0b3d69 100644 --- a/packages/core/src/docs/data-model/text-x/__tests__/apply.spec.ts +++ b/packages/core/src/docs/data-model/text-x/__tests__/apply.spec.ts @@ -17,7 +17,8 @@ import type { IDocumentBody } from '../../../../types/interfaces'; import type { TextXAction } from '../action-types'; import { describe, expect, it } from 'vitest'; -import { BooleanNumber } from '../../../../types/enum'; +import { UpdateDocsAttributeType } from '../../../../shared'; +import { BooleanNumber, HorizontalAlign } from '../../../../types/enum'; import { TextXActionType } from '../action-types'; import { TextX } from '../text-x'; @@ -55,6 +56,28 @@ function getDefaultDocWithLength2() { return doc; } +function getDefaultDocWithParagraph() { + const doc: IDocumentBody = { + dataStream: 'w\r\n', + textRuns: [ + { + st: 0, + ed: 1, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + paragraphs: [ + { + startIndex: 1, + }, + ], + }; + + return doc; +} + describe('apply method', () => { it('should get the same result when apply two actions by order OR composed first case 1', () => { const actionsA: TextXAction[] = [ @@ -167,12 +190,6 @@ describe('apply method', () => { const resultC = TextX.apply(doc3, composedAction1); const resultD = TextX.apply(doc4, composedAction2); - // console.log(JSON.stringify(resultA, null, 2)); - // console.log(JSON.stringify(resultB, null, 2)); - - // console.log(JSON.stringify(composedAction2, null, 2)); - // console.log(JSON.stringify(resultC, null, 2)); - expect(resultA).toEqual(resultB); expect(resultC).toEqual(resultD); expect(resultA).toEqual(resultC); @@ -250,4 +267,139 @@ describe('apply method', () => { expect(resultA).toEqual(resultC); expect(composedAction1).toEqual(composedAction2); }); + + it('should get the same result when set paragraph align type at 2 clients', () => { + const actionsA: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + }, { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + body: { + dataStream: '', + paragraphs: [{ + startIndex: 0, + paragraphStyle: { + horizontalAlign: HorizontalAlign.LEFT, + }, + }], + }, + }, + ]; + + const actionsB: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + }, { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + body: { + dataStream: '', + paragraphs: [{ + startIndex: 0, + paragraphStyle: { + horizontalAlign: HorizontalAlign.RIGHT, + }, + }], + }, + }, + ]; + + const doc1 = getDefaultDocWithParagraph(); + const doc2 = getDefaultDocWithParagraph(); + const doc3 = getDefaultDocWithParagraph(); + const doc4 = getDefaultDocWithParagraph(); + + 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')); + + 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); + }); + + it('should get the same result when set paragraph align with REPLACE cover type at 2 clients', () => { + const actionsA: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + }, { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + coverType: UpdateDocsAttributeType.REPLACE, + body: { + dataStream: '', + paragraphs: [{ + startIndex: 0, + paragraphStyle: { + horizontalAlign: HorizontalAlign.LEFT, + }, + }], + }, + }, + ]; + + const actionsB: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + }, { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + coverType: UpdateDocsAttributeType.REPLACE, + body: { + dataStream: '', + paragraphs: [{ + startIndex: 0, + paragraphStyle: { + horizontalAlign: HorizontalAlign.RIGHT, + }, + }], + }, + }, + ]; + + const doc1 = getDefaultDocWithParagraph(); + const doc2 = getDefaultDocWithParagraph(); + const doc3 = getDefaultDocWithParagraph(); + const doc4 = getDefaultDocWithParagraph(); + + 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')); + + const resultC = TextX.apply(doc3, composedAction1); + const resultD = TextX.apply(doc4, composedAction2); + + // console.log(JSON.stringify(resultA, null, 2)); + // console.log(JSON.stringify(resultB, null, 2)); + + // console.log('composedAction1', JSON.stringify(composedAction1, null, 2)); + + // console.log(JSON.stringify(resultC, null, 2)); + + 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__/transform.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/transform.spec.ts index 2191f6bc7d7..299e90b355a 100644 --- a/packages/core/src/docs/data-model/text-x/__tests__/transform.spec.ts +++ b/packages/core/src/docs/data-model/text-x/__tests__/transform.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'; import { TextXActionType } from '../action-types'; import { TextX } from '../text-x'; @@ -673,6 +674,93 @@ describe('transform()', () => { expect(TextX._transform(actionsA, actionsB, 'left')).toEqual(expectedActionsWithPriorityTrue); }); + it('retain + retain with paragraph and REPLACE type', () => { + const actionsA: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + coverType: UpdateDocsAttributeType.REPLACE, + body: { + dataStream: '', + paragraphs: [ + { + startIndex: 0, + paragraphStyle: { + lineSpacing: 1, + }, + }, + ], + }, + }, + ]; + + const actionsB: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + coverType: UpdateDocsAttributeType.REPLACE, + body: { + dataStream: '', + paragraphs: [ + { + startIndex: 0, + paragraphStyle: { + lineSpacing: 5, + spaceBelow: { v: 6 }, + }, + }, + ], + }, + }, + ]; + + const expectedActionsWithPriorityTrue: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + coverType: UpdateDocsAttributeType.REPLACE, + body: { + dataStream: '', + paragraphs: [ + { + startIndex: 0, + paragraphStyle: { + lineSpacing: 1, + }, + }, + ], + }, + }, + ]; + + const expectedActionsWithPriorityFalse: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + coverType: UpdateDocsAttributeType.REPLACE, + body: { + dataStream: '', + paragraphs: [ + { + startIndex: 0, + paragraphStyle: { + lineSpacing: 5, + spaceBelow: { v: 6 }, + }, + }, + ], + }, + }, + ]; + + expect(TextX._transform(actionsA, actionsB, 'right')).toEqual(expectedActionsWithPriorityFalse); + expect(TextX._transform(actionsA, actionsB, 'left')).toEqual(expectedActionsWithPriorityTrue); + }); + it('insert after the retain attributes', () => { const actionA: TextXAction[] = [{ t: TextXActionType.RETAIN, 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 7dd93b5a253..a0eb4accda5 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 @@ -14,14 +14,14 @@ * limitations under the License. */ -import { Tools } from '../../../shared/tools'; -import { UpdateDocsAttributeType } from '../../../shared/command-enum'; import type { IDocumentBody } from '../../../types/interfaces/i-document-data'; -import { type IDeleteAction, type IInsertAction, type IRetainAction, type TextXAction, TextXActionType } from './action-types'; +import { UpdateDocsAttributeType } from '../../../shared/command-enum'; +import { Tools } from '../../../shared/tools'; import { ActionIterator } from './action-iterator'; -import { composeBody, getBodySlice, isUselessRetainAction } from './utils'; +import { type IDeleteAction, type IInsertAction, type IRetainAction, type TextXAction, TextXActionType } from './action-types'; import { textXApply } from './apply'; import { transformBody } from './transform-utils'; +import { composeBody, getBodySlice, isUselessRetainAction } from './utils'; function onlyHasDataStream(body: IDocumentBody) { return Object.keys(body).length === 1; @@ -118,6 +118,7 @@ export class TextX { * 2) If the other body property exists, then execute the TransformBody logic to override it */ // priority - if true, this actions takes priority over other, that is, this actions are considered to happen "first". + // thisActions is the target action. static transform(thisActions: TextXAction[], otherActions: TextXAction[], priority: TPriority = 'right'): TextXAction[] { return this._transform(otherActions, thisActions, priority === 'left' ? 'right' : 'left'); } 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 35d3c5faaf0..915d90c102f 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 @@ -17,7 +17,7 @@ import type { Nullable } from '../../../shared'; import type { IDocumentBody, IParagraph, IParagraphStyle, ITextRun, ITextStyle } from '../../../types/interfaces'; import type { IRetainAction } from './action-types'; -import { Tools } from '../../../shared'; +import { Tools, UpdateDocsAttributeType } from '../../../shared'; import { normalizeTextRuns } from './apply-utils/common'; enum TextXTransformType { @@ -153,7 +153,8 @@ function transformTextRuns(originTextRuns: ITextRun[], targetTextRuns: ITextRun[ function transformParagraph( originParagraph: IParagraph, targetParagraph: IParagraph, - transformType: TextXTransformType + transformType: TextXTransformType, + coverType: UpdateDocsAttributeType = UpdateDocsAttributeType.COVER ): IParagraph { const paragraph: IParagraph = { startIndex: targetParagraph.startIndex, @@ -165,16 +166,22 @@ function transformParagraph( ...targetParagraph.paragraphStyle, }; } else { - paragraph.paragraphStyle = { - ...targetParagraph.paragraphStyle, - }; + if (coverType === UpdateDocsAttributeType.REPLACE) { + paragraph.paragraphStyle = transformType === TextXTransformType.COVER_ONLY_NOT_EXISTED + ? { ...originParagraph.paragraphStyle } + : { ...targetParagraph.paragraphStyle }; + } else { + paragraph.paragraphStyle = { + ...targetParagraph.paragraphStyle, + }; - if (transformType === TextXTransformType.COVER_ONLY_NOT_EXISTED) { - const keys = Object.keys(originParagraph.paragraphStyle); + if (transformType === TextXTransformType.COVER_ONLY_NOT_EXISTED) { + 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]; + for (const key of keys) { + if (paragraph.paragraphStyle[key as keyof IParagraphStyle]) { + delete paragraph.paragraphStyle[key as keyof IParagraphStyle]; + } } } } @@ -192,17 +199,22 @@ function transformParagraph( return paragraph; } +// eslint-disable-next-line max-lines-per-function export function transformBody( thisAction: IRetainAction, otherAction: IRetainAction, priority: boolean = false ): IDocumentBody { - const { body: thisBody } = thisAction; - const { body: otherBody } = otherAction; + const { body: thisBody, coverType: thisCoverType } = thisAction; + const { body: otherBody, coverType: otherCoverType } = otherAction; if (thisBody == null || thisBody.dataStream !== '' || otherBody == null || otherBody.dataStream !== '') { throw new Error('Data stream is not supported in transform.'); } + if (thisCoverType !== otherCoverType) { + throw new Error('Cover type is not consistent.'); + } + const retBody: IDocumentBody = { dataStream: '', }; @@ -241,10 +253,11 @@ export function transformBody( paragraph = transformParagraph( thisParagraph, otherParagraph, - TextXTransformType.COVER_ONLY_NOT_EXISTED + TextXTransformType.COVER_ONLY_NOT_EXISTED, + thisCoverType ); } else { - paragraph = transformParagraph(thisParagraph, otherParagraph, TextXTransformType.COVER); + paragraph = transformParagraph(thisParagraph, otherParagraph, TextXTransformType.COVER, thisCoverType); } paragraphs.push(paragraph); diff --git a/packages/docs-ui/src/commands/commands/doc-paragraph-setting.command.ts b/packages/docs-ui/src/commands/commands/doc-paragraph-setting.command.ts index 8a85fa9c98e..be7bd08022e 100644 --- a/packages/docs-ui/src/commands/commands/doc-paragraph-setting.command.ts +++ b/packages/docs-ui/src/commands/commands/doc-paragraph-setting.command.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { CommandType, ICommandService, IUniverInstanceService, JSONX, MemoryCursor, TextX, TextXActionType, UniverInstanceType, UpdateDocsAttributeType } from '@univerjs/core'; -import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; import type { DocumentDataModel, IAccessor, ICommand, IMutationInfo, IParagraphStyle } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; +import { CommandType, ICommandService, IUniverInstanceService, JSONX, MemoryCursor, TextX, TextXActionType, UniverInstanceType, UpdateDocsAttributeType } from '@univerjs/core'; +import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; import { getRichTextEditPath } from '../util'; import { getParagraphsInRanges } from './list.command';