Skip to content

Commit

Permalink
Content Model Cache improvement - Step 5: Port roosterjs-content-mode…
Browse files Browse the repository at this point in the history
…l-dom package (#2648)

* Readonly types (3rd try

* Improve

* fix build

* Improve

* improve

* Improve

* Add shallow mutable type

* improve

* Improve

* improve

* improve

* add test

* Readonly types step 2

* Readonly types step 3

* Readonly type step 4

* add test

* Improve

* improve

* improve

* Readonly types step 5: dom package

* add change

* improve

* improve

* Improve

* improve

* fix test

* Improve

* fix build

* improve
  • Loading branch information
JiuqingSong authored May 23, 2024
1 parent 2aa32f4 commit 2b74dcb
Show file tree
Hide file tree
Showing 38 changed files with 548 additions and 359 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import {
createListLevel,
getOperationalBlocks,
isBlockGroupOfType,
mutateBlock,
parseValueWithUnit,
updateListMetadata,
} from 'roosterjs-content-model-dom';
import type {
ContentModelBlock,
ContentModelBlockFormat,
ContentModelBlockGroup,
ContentModelDocument,
ContentModelListItem,
ContentModelListLevel,
FormatContentModelContext,
ReadonlyContentModelBlock,
ReadonlyContentModelBlockGroup,
ReadonlyContentModelDocument,
ReadonlyContentModelListItem,
} from 'roosterjs-content-model-types';

const IndentStepInPixel = 40;
Expand All @@ -26,7 +28,7 @@ const IndentStepInPixel = 40;
* Set indentation for selected list items or paragraphs
*/
export function setModelIndentation(
model: ContentModelDocument,
model: ReadonlyContentModelDocument,
indentation: 'indent' | 'outdent',
length: number = IndentStepInPixel,
context?: FormatContentModelContext
Expand All @@ -37,7 +39,7 @@ export function setModelIndentation(
['TableCell']
);
const isIndent = indentation == 'indent';
const modifiedBlocks: ContentModelBlock[] = [];
const modifiedBlocks: ReadonlyContentModelBlock[] = [];

paragraphOrListItem.forEach(({ block, parent, path }) => {
if (isBlockGroupOfType<ContentModelListItem>(block, 'ListItem')) {
Expand Down Expand Up @@ -89,12 +91,12 @@ export function setModelIndentation(
}
}
} else if (block) {
let currentBlock: ContentModelBlock = block;
let currentParent: ContentModelBlockGroup = parent;
let currentBlock: ReadonlyContentModelBlock = block;
let currentParent: ReadonlyContentModelBlockGroup = parent;

while (currentParent && modifiedBlocks.indexOf(currentBlock) < 0) {
const index = path.indexOf(currentParent);
const { format } = currentBlock;
const { format } = mutateBlock(currentBlock);
const newValue = calculateMarginValue(format, isIndent, length);

if (newValue !== null) {
Expand Down Expand Up @@ -124,7 +126,7 @@ export function setModelIndentation(
return paragraphOrListItem.length > 0;
}

function isSelected(listItem: ContentModelListItem) {
function isSelected(listItem: ReadonlyContentModelListItem) {
return listItem.blocks.some(block => {
if (block.blockType == 'Paragraph') {
return block.segments.some(segment => segment.isSelected);
Expand All @@ -137,9 +139,9 @@ function isSelected(listItem: ContentModelListItem) {
* Otherwise, the margin of the first item will be changed, and the sub list will be created, creating a unintentional margin difference between the list items.
*/
function isMultilevelSelection(
model: ContentModelDocument,
listItem: ContentModelListItem,
parent: ContentModelBlockGroup
model: ReadonlyContentModelDocument,
listItem: ReadonlyContentModelListItem,
parent: ReadonlyContentModelBlockGroup
) {
const listIndex = parent.blocks.indexOf(listItem);
for (let i = listIndex - 1; i >= 0; i--) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import {
deleteSelection,
getClosestAncestorBlockGroupIndex,
setSelection,
mutateBlock,
} from 'roosterjs-content-model-dom';
import type {
ContentModelBlock,
ContentModelBlockGroup,
ContentModelDocument,
ContentModelEntity,
ContentModelParagraph,
FormatContentModelContext,
InsertEntityPosition,
InsertPoint,
ReadonlyContentModelBlock,
ShallowMutableContentModelBlock,
ShallowMutableContentModelBlockGroup,
ShallowMutableContentModelParagraph,
} from 'roosterjs-content-model-types';

/**
Expand All @@ -30,7 +32,7 @@ export function insertEntityModel(
context?: FormatContentModelContext,
insertPointOverride?: InsertPoint
) {
let blockParent: ContentModelBlockGroup | undefined;
let blockParent: ShallowMutableContentModelBlockGroup | undefined;
let blockIndex = -1;
let insertPoint: InsertPoint | null;

Expand All @@ -57,9 +59,10 @@ export function insertEntityModel(
position == 'root'
? getClosestAncestorBlockGroupIndex(path, ['TableCell', 'Document'])
: 0;
blockParent = path[pathIndex];
blockParent = mutateBlock(path[pathIndex]);

const child = path[pathIndex - 1];
const directChild: ContentModelBlock =
const directChild: ReadonlyContentModelBlock =
child?.blockGroupType == 'FormatContainer' ||
child?.blockGroupType == 'General' ||
child?.blockGroupType == 'ListItem'
Expand All @@ -71,16 +74,16 @@ export function insertEntityModel(
}

if (blockIndex >= 0 && blockParent) {
const blocksToInsert: ContentModelBlock[] = [];
let nextParagraph: ContentModelParagraph | undefined;
const blocksToInsert: ShallowMutableContentModelBlock[] = [];
let nextParagraph: ShallowMutableContentModelParagraph | undefined;

if (isBlock) {
const nextBlock = blockParent.blocks[blockIndex];

blocksToInsert.push(entityModel);

if (nextBlock?.blockType == 'Paragraph') {
nextParagraph = nextBlock;
nextParagraph = mutateBlock(nextBlock);
} else if (!nextBlock || nextBlock.blockType == 'Entity' || focusAfterEntity) {
nextParagraph = createParagraph(false /*isImplicit*/, {}, model.format);
nextParagraph.segments.push(createBr(model.format));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ describe('adjustImageSelection', () => {
format: {},
src: 'img2',
dataset: {},
isSelectedAsImageSelection: false,
},
{
segmentType: 'Text',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ describe('adjustLinkSelection', () => {
link: link,
dataset: {},
isSelected: true,
isSelectedAsImageSelection: false,
},
{
segmentType: 'Text',
Expand Down Expand Up @@ -228,7 +227,6 @@ describe('adjustLinkSelection', () => {
link: link,
dataset: {},
isSelected: true,
isSelectedAsImageSelection: false,
},
{
segmentType: 'Text',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ describe('removeLink', () => {
dataset: {},
format: {},
isSelected: true,
isSelectedAsImageSelection: false,
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import {
getClosestAncestorBlockGroupIndex,
hasSelectionInBlock,
hasSelectionInBlockGroup,
mutateBlock,
} from 'roosterjs-content-model-dom';
import type {
ContentModelBlock,
DeleteSelectionContext,
DeleteSelectionStep,
ReadonlyContentModelBlock,
} from 'roosterjs-content-model-types';

function isEmptyBlock(block: ContentModelBlock | undefined): boolean {
function isEmptyBlock(block: ReadonlyContentModelBlock | undefined): boolean {
if (block && block.blockType == 'Paragraph') {
return block.segments.every(
segment => segment.segmentType !== 'SelectionMarker' && segment.segmentType == 'Br'
Expand Down Expand Up @@ -53,7 +54,7 @@ export const deleteEmptyList: DeleteSelectionStep = (context: DeleteSelectionCon
nextBlock &&
isEmptyBlock(nextBlock)
) {
item.levels = [];
mutateBlock(item).levels = [];
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { ContentModelBlock } from 'roosterjs-content-model-types';
import { mutateBlock } from '../common/mutate';
import type { ReadonlyContentModelBlock } from 'roosterjs-content-model-types';

/**
* For a given block, if it is a paragraph, set it to be not-implicit
* @param block The block to check
*/
export function setParagraphNotImplicit(block: ContentModelBlock) {
export function setParagraphNotImplicit(block: ReadonlyContentModelBlock) {
if (block.blockType == 'Paragraph' && block.isImplicit) {
block.isImplicit = false;
mutateBlock(block).isImplicit = false;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { isBlockEmpty } from './isEmpty';
import { mutateBlock } from './mutate';
import { normalizeParagraph } from './normalizeParagraph';
import { unwrapBlock } from './unwrapBlock';
import type { ContentModelBlockGroup } from 'roosterjs-content-model-types';
import type { ReadonlyContentModelBlockGroup } from 'roosterjs-content-model-types';

/**
* For a given content model, normalize it to make the model be consistent.
Expand All @@ -12,7 +13,7 @@ import type { ContentModelBlockGroup } from 'roosterjs-content-model-types';
* - For an empty block, remove it
* @param group The root level block group of content model to normalize
*/
export function normalizeContentModel(group: ContentModelBlockGroup) {
export function normalizeContentModel(group: ReadonlyContentModelBlockGroup) {
for (let i = group.blocks.length - 1; i >= 0; i--) {
const block = group.blocks[i];

Expand Down Expand Up @@ -40,7 +41,7 @@ export function normalizeContentModel(group: ContentModelBlockGroup) {
}

if (isBlockEmpty(block)) {
group.blocks.splice(i, 1);
mutateBlock(group).blocks.splice(i, 1);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ import { areSameFormats } from '../../domToModel/utils/areSameFormats';
import { createBr } from '../creators/createBr';
import { isSegmentEmpty } from './isEmpty';
import { isWhiteSpacePreserved } from '../../domUtils/isWhiteSpacePreserved';
import { mutateBlock, mutateSegment } from './mutate';
import { normalizeAllSegments } from './normalizeSegment';
import type {
ContentModelParagraph,
ContentModelSegment,
ContentModelSegmentFormat,
ReadonlyContentModelParagraph,
ReadonlyContentModelSegment,
} from 'roosterjs-content-model-types';

/**
* @param paragraph The paragraph to normalize
* Normalize a paragraph. If it is empty, add a BR segment to make sure it can insert content
*/
export function normalizeParagraph(paragraph: ContentModelParagraph) {
export function normalizeParagraph(paragraph: ReadonlyContentModelParagraph) {
const segments = paragraph.segments;

if (!paragraph.isImplicit && segments.length > 0) {
Expand All @@ -24,7 +25,7 @@ export function normalizeParagraph(paragraph: ContentModelParagraph) {
last.segmentType == 'SelectionMarker' &&
(!secondLast || secondLast.segmentType == 'Br')
) {
segments.push(createBr(last.format));
mutateBlock(paragraph).segments.push(createBr(last.format));
} else if (segments.length > 1 && segments[segments.length - 1].segmentType == 'Br') {
const noMarkerSegments = segments.filter(x => x.segmentType != 'SelectionMarker');

Expand All @@ -34,7 +35,7 @@ export function normalizeParagraph(paragraph: ContentModelParagraph) {
noMarkerSegments.length > 1 &&
noMarkerSegments[noMarkerSegments.length - 2].segmentType != 'Br'
) {
segments.pop();
mutateBlock(paragraph).segments.pop();
}
}
}
Expand All @@ -50,20 +51,21 @@ export function normalizeParagraph(paragraph: ContentModelParagraph) {
moveUpSegmentFormat(paragraph);
}

function removeEmptySegments(block: ContentModelParagraph) {
function removeEmptySegments(block: ReadonlyContentModelParagraph) {
for (let j = block.segments.length - 1; j >= 0; j--) {
if (isSegmentEmpty(block.segments[j])) {
block.segments.splice(j, 1);
mutateBlock(block).segments.splice(j, 1);
}
}
}

function removeEmptyLinks(paragraph: ContentModelParagraph) {
function removeEmptyLinks(paragraph: ReadonlyContentModelParagraph) {
const marker = paragraph.segments.find(x => x.segmentType == 'SelectionMarker');
if (marker) {
const markerIndex = paragraph.segments.indexOf(marker);
const prev = paragraph.segments[markerIndex - 1];
const next = paragraph.segments[markerIndex + 1];

if (
(prev &&
!prev.link &&
Expand All @@ -76,7 +78,9 @@ function removeEmptyLinks(paragraph: ContentModelParagraph) {
!next.link &&
areSameFormats(next.format, marker.format))
) {
delete marker.link;
mutateSegment(paragraph, marker, mutableMarker => {
delete mutableMarker.link;
});
}
}
}
Expand All @@ -85,7 +89,7 @@ type FormatsToMoveUp = 'fontFamily' | 'fontSize' | 'textColor';
const formatsToMoveUp: FormatsToMoveUp[] = ['fontFamily', 'fontSize', 'textColor'];

// When all segments are sharing the same segment format (font name, size and color), we can move its format to paragraph
function moveUpSegmentFormat(paragraph: ContentModelParagraph) {
function moveUpSegmentFormat(paragraph: ReadonlyContentModelParagraph) {
if (!paragraph.decorator) {
const segments = paragraph.segments.filter(x => x.segmentType != 'SelectionMarker');
const target = paragraph.segmentFormat || {};
Expand All @@ -96,13 +100,13 @@ function moveUpSegmentFormat(paragraph: ContentModelParagraph) {
});

if (changed) {
paragraph.segmentFormat = target;
mutateBlock(paragraph).segmentFormat = target;
}
}
}

function internalMoveUpSegmentFormat(
segments: ContentModelSegment[],
segments: ReadonlyContentModelSegment[],
target: ContentModelSegmentFormat,
formatKey: FormatsToMoveUp
): boolean {
Expand Down
Loading

0 comments on commit 2b74dcb

Please sign in to comment.