Skip to content

Commit

Permalink
Content Model: Advanced cache (#2083)
Browse files Browse the repository at this point in the history
* Content Model Customization refactor

* fix build

* improve

* Content Model Customization refactor 2: Add default config

* fix build

* Content Model: Persist cache 1

* fix build

* improve

* Content Model: Cache 2

* Fix test

* Fix build

* improve

* Improve

* improve

* Improve

* fix test

* Do not restore cached selection when call select

* Content Model: Add model into ContentChangedEvent

* fix build

* add demo site page

* Improve

* Content Model: pass out segment nodes

* cache 8

* fix build

* fix test

* Improve

* improve

* fix test

* Improve

* fix test

* Improve "checkDependency"

* Content Model: Clear table selection fix

* fix build

* Content Model: Improve adjustWordSelection

* add test

* add test

* fix test

* Improve

* retrigger check step

* retrigger check step
  • Loading branch information
JiuqingSong authored Sep 25, 2023
1 parent d9e8e89 commit 00558e8
Show file tree
Hide file tree
Showing 50 changed files with 3,140 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ const initialState: BuildInPluginState = {
linkTitle: 'Ctrl+Click to follow the link:' + UrlPlaceholder,
watermarkText: 'Type content here ...',
forcePreserveRatio: false,
experimentalFeatures: [ExperimentalFeatures.ContentModelPaste],
experimentalFeatures: [
ExperimentalFeatures.ContentModelPaste,
ExperimentalFeatures.ReusableContentModelV2,
],
isRtl: false,
tableFeaturesContainerSelector: '#' + 'EditorContainer',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const FeatureNames: Partial<Record<ExperimentalFeatures, string>> = {
'Delete a table selected with the table selector pressing Backspace key',
[ExperimentalFeatures.ContentModelPaste]: 'Paste with content model',
[ExperimentalFeatures.DisableListChain]: 'Disable list chain functionality',
[ExperimentalFeatures.ReusableContentModelV2]:
'Reuse existing DOM structure if possible, and update the model when content or selection is changed',
};

export default class ContentModelExperimentalFeaturesPane extends React.Component<
Expand All @@ -36,7 +38,7 @@ export default class ContentModelExperimentalFeaturesPane extends React.Componen
id={name}
onChange={() => this.onClick(name)}
/>
<label htmlFor={name}>{FeatureNames[name]}</label>
<label htmlFor={name}>{name + ': ' + FeatureNames[name]}</label>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export const brProcessor: ElementProcessor<HTMLBRElement> = (group, element, con
br.isSelected = true;
}

addSegment(group, br, context.blockFormat);
const paragraph = addSegment(group, br, context.blockFormat);
context.domIndexer?.onSegment(element, paragraph, [br]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export const entityProcessor: ElementProcessor<HTMLElement> = (group, element, c
if (isBlockEntity) {
addBlock(group, entityModel);
} else {
addSegment(group, entityModel);
const paragraph = addSegment(group, entityModel);
context.domIndexer?.onSegment(element, paragraph, [entityModel]);
}
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ const generalSegmentProcessor: ElementProcessor<HTMLElement> = (group, element,
const isSelectedBefore = context.isInSelection;

addDecorators(segment, context);
addSegment(group, segment);
const paragraph = addSegment(group, segment);
context.domIndexer?.onSegment(element, paragraph, [segment]);

stackFormat(
context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const imageProcessor: ElementProcessor<HTMLImageElement> = (group, elemen
image.isSelected = true;
}

addSegment(group, image);
const paragraph = addSegment(group, image);
context.domIndexer?.onSegment(element, paragraph, [image]);
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export const tableProcessor: ElementProcessor<HTMLTableElement> = (
table.cachedElement = tableElement;
}

context.domIndexer?.onTable(tableElement, table);

parseFormat(tableElement, context.formatParsers.table, table.format, context);
parseFormat(tableElement, context.formatParsers.tableBorder, table.format, context);
parseFormat(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { addDecorators } from '../../modelApi/common/addDecorators';
import { addSegment } from '../../modelApi/common/addSegment';
import { addSelectionMarker } from '../utils/addSelectionMarker';
import { areSameFormats } from '../utils/areSameFormats';
import { createText } from '../../modelApi/creators/createText';
import { ensureParagraph } from '../../modelApi/common/ensureParagraph';
import { getRegularSelectionOffsets } from '../utils/getRegularSelectionOffsets';
import { hasSpacesOnly } from '../../modelApi/common/hasSpacesOnly';
import {
ContentModelBlockGroup,
ContentModelParagraph,
ContentModelText,
DomToModelContext,
ElementProcessor,
} from 'roosterjs-content-model-types';
Expand All @@ -21,9 +23,12 @@ export const textProcessor: ElementProcessor<Text> = (
) => {
let txt = textNode.nodeValue || '';
let [txtStartOffset, txtEndOffset] = getRegularSelectionOffsets(context, textNode);
const segments: (ContentModelText | undefined)[] = [];
const paragraph = ensureParagraph(group, context.blockFormat);

if (txtStartOffset >= 0) {
addTextSegment(group, txt.substring(0, txtStartOffset), context);
const subText = txt.substring(0, txtStartOffset);
segments.push(addTextSegment(group, subText, paragraph, context));
context.isInSelection = true;

addSelectionMarker(group, context);
Expand All @@ -33,7 +38,8 @@ export const textProcessor: ElementProcessor<Text> = (
}

if (txtEndOffset >= 0) {
addTextSegment(group, txt.substring(0, txtEndOffset), context);
const subText = txt.substring(0, txtEndOffset);
segments.push(addTextSegment(group, subText, paragraph, context));

if (context.rangeEx && !context.rangeEx.areAllCollapsed) {
addSelectionMarker(group, context);
Expand All @@ -43,32 +49,32 @@ export const textProcessor: ElementProcessor<Text> = (
txt = txt.substring(txtEndOffset);
}

addTextSegment(group, txt, context);
segments.push(addTextSegment(group, txt, paragraph, context));
context.domIndexer?.onSegment(
textNode,
paragraph,
segments.filter((x): x is ContentModelText => !!x)
);
};

// When we see these values of white-space style, need to preserve spaces and line-breaks and let browser handle it for us.
const WhiteSpaceValuesNeedToHandle = ['pre', 'pre-wrap', 'pre-line', 'break-spaces'];

function addTextSegment(group: ContentModelBlockGroup, text: string, context: DomToModelContext) {
if (text) {
const lastBlock = group.blocks[group.blocks.length - 1];
const paragraph = lastBlock?.blockType == 'Paragraph' ? lastBlock : null;
const lastSegment = paragraph?.segments[paragraph.segments.length - 1];
function addTextSegment(
group: ContentModelBlockGroup,
text: string,
paragraph: ContentModelParagraph,
context: DomToModelContext
): ContentModelText | undefined {
let textModel: ContentModelText | undefined;

if (text) {
if (
lastSegment?.segmentType == 'Text' &&
!!lastSegment.isSelected == !!context.isInSelection &&
areSameFormats(lastSegment.format, context.segmentFormat) &&
areSameFormats(lastSegment.link || {}, context.link.format || {}) &&
areSameFormats(lastSegment.code || {}, context.code.format || {})
) {
lastSegment.text += text;
} else if (
!hasSpacesOnly(text) ||
paragraph?.segments.length! > 0 ||
(paragraph?.segments.length ?? 0) > 0 ||
WhiteSpaceValuesNeedToHandle.indexOf(paragraph?.format.whiteSpace || '') >= 0
) {
const textModel = createText(text, context.segmentFormat);
textModel = createText(text, context.segmentFormat);

if (context.isInSelection) {
textModel.isSelected = true;
Expand All @@ -79,4 +85,6 @@ function addTextSegment(group: ContentModelBlockGroup, text: string, context: Do
addSegment(group, textModel, context.blockFormat);
}
}

return textModel;
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export { createListLevel } from './modelApi/creators/createListLevel';
export { addBlock } from './modelApi/common/addBlock';
export { addCode } from './modelApi/common/addDecorators';
export { addLink } from './modelApi/common/addDecorators';
export { ensureParagraph } from './modelApi/common/ensureParagraph';

export { normalizeContentModel } from './modelApi/common/normalizeContentModel';
export { isGeneralSegment } from './modelApi/common/isGeneralSegment';
export { unwrapBlock } from './modelApi/common/unwrapBlock';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { addBlock } from './addBlock';
import { createParagraph } from '../creators/createParagraph';
import { ensureParagraph } from './ensureParagraph';
import type {
ContentModelBlockFormat,
ContentModelBlockGroup,
Expand All @@ -12,22 +11,14 @@ import type {
* @param group The parent block group of the paragraph to add segment into
* @param newSegment The segment to add
* @param blockFormat The block format used for creating a new paragraph when need
* @returns The parent paragraph where the segment is added to
*/
export function addSegment(
group: ContentModelBlockGroup,
newSegment: ContentModelSegment,
blockFormat?: ContentModelBlockFormat
) {
const lastBlock = group.blocks[group.blocks.length - 1];
let paragraph: ContentModelParagraph;

if (lastBlock?.blockType == 'Paragraph') {
paragraph = lastBlock;
} else {
paragraph = createParagraph(true, blockFormat);
addBlock(group, paragraph);
}

): ContentModelParagraph {
const paragraph = ensureParagraph(group, blockFormat);
const lastSegment = paragraph.segments[paragraph.segments.length - 1];

if (newSegment.segmentType == 'SelectionMarker') {
Expand All @@ -41,4 +32,6 @@ export function addSegment(

paragraph.segments.push(newSegment);
}

return paragraph;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { addBlock } from './addBlock';
import { createParagraph } from '../creators/createParagraph';
import {
ContentModelBlockFormat,
ContentModelBlockGroup,
ContentModelParagraph,
} from 'roosterjs-content-model-types';

/**
* Ensure there is a Paragraph that can insert segments in a Content Model Block Group
* @param group The parent block group of the target paragraph
* @param blockFormat Format of the paragraph. This is only used if we need to create a new paragraph
*/
export function ensureParagraph(
group: ContentModelBlockGroup,
blockFormat?: ContentModelBlockFormat
): ContentModelParagraph {
const lastBlock = group.blocks[group.blocks.length - 1];

if (lastBlock?.blockType == 'Paragraph') {
return lastBlock;
} else {
const paragraph = createParagraph(true, blockFormat);
addBlock(group, paragraph);

return paragraph;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export const handleParagraph: ContentModelBlockHandler<ContentModelParagraph> =
paragraph.segments.forEach(segment => {
const newSegments: Node[] = [];
context.modelHandlers.segment(doc, parent, segment, context, newSegments);

newSegments.forEach(node => {
context.domIndexer?.onSegment(node, paragraph, [segment]);
});
});
}
};
Expand Down Expand Up @@ -103,6 +107,11 @@ export const handleParagraph: ContentModelBlockHandler<ContentModelParagraph> =
// to make sure the value is correct.
refNode = container.nextSibling;

if (container) {
context.onNodeCreated?.(paragraph, container);
context.domIndexer?.onParagraph(container);
}

if (needParagraphWrapper) {
if (context.allowCacheElement) {
paragraph.cachedElement = container;
Expand All @@ -114,9 +123,5 @@ export const handleParagraph: ContentModelBlockHandler<ContentModelParagraph> =
});
}

if (container) {
context.onNodeCreated?.(paragraph, container);
}

return refNode;
};
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,7 @@ export const handleTable: ContentModelBlockHandler<ContentModelTable> = (
}
}

context.domIndexer?.onTable(tableNode, table);

return refNode;
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { brProcessor } from '../../../lib/domToModel/processors/brProcessor';
import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument';
import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext';
import { DomToModelContext } from 'roosterjs-content-model-types';
import {
ContentModelBr,
ContentModelDomIndexer,
ContentModelParagraph,
DomToModelContext,
} from 'roosterjs-content-model-types';

describe('brProcessor', () => {
let context: DomToModelContext;
Expand Down Expand Up @@ -59,4 +64,37 @@ describe('brProcessor', () => {
],
});
});

it('Br with domIndexer', () => {
const doc = createContentModelDocument();
const br = document.createElement('br');
const onSegmentSpy = jasmine.createSpy('onSegment');
const domIndexer: ContentModelDomIndexer = {
onParagraph: null!,
onSegment: onSegmentSpy,
onTable: null!,
reconcileSelection: null!,
};

context.domIndexer = domIndexer;

brProcessor(doc, br, context);

const brModel: ContentModelBr = {
segmentType: 'Br',
format: {},
};
const paragraphModel: ContentModelParagraph = {
blockType: 'Paragraph',
isImplicit: true,
segments: [brModel],
format: {},
};

expect(doc).toEqual({
blockGroupType: 'Document',
blocks: [paragraphModel],
});
expect(onSegmentSpy).toHaveBeenCalledWith(br, paragraphModel, [brModel]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ describe('childProcessor', () => {
isSelected: true,
format: {},
},
{ segmentType: 'Text', text: 'test2test3', format: {} },
{ segmentType: 'Text', text: 'test2', format: {} },
{ segmentType: 'Text', text: 'test3', format: {} },
],
isImplicit: true,
format: {},
Expand Down
Loading

0 comments on commit 00558e8

Please sign in to comment.