Skip to content

Commit

Permalink
Content Model: Support format parser and applier for text node (#2334)
Browse files Browse the repository at this point in the history
* Support format parser and applier for text node

* fix build
  • Loading branch information
JiuqingSong authored Jan 17, 2024
1 parent b4e7ac7 commit 9d98823
Show file tree
Hide file tree
Showing 12 changed files with 224 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
FormatParser,
FormatParsers,
FormatParsersPerCategory,
TextFormatParser,
} from 'roosterjs-content-model-types';

/**
Expand Down Expand Up @@ -118,22 +119,35 @@ export function buildFormatParsers(
): FormatParsersPerCategory {
const combinedOverrides = Object.assign({}, ...overrides);

return getObjectKeys(defaultFormatKeysPerCategory).reduce((result, key) => {
const value = defaultFormatKeysPerCategory[key]
.map(
formatKey =>
(combinedOverrides[formatKey] === undefined
? defaultFormatParsers[formatKey]
: combinedOverrides[formatKey]) as FormatParser<any>
)
.concat(
...additionalParsersArray.map(
parsers => (parsers?.[key] ?? []) as FormatParser<any>[]
const result = getObjectKeys(defaultFormatKeysPerCategory).reduce(
(result, key) => {
const value = defaultFormatKeysPerCategory[key]
.map(
formatKey =>
(combinedOverrides[formatKey] === undefined
? defaultFormatParsers[formatKey]
: combinedOverrides[formatKey]) as FormatParser<any>
)
);
.concat(
...additionalParsersArray.map(
parsers => (parsers?.[key] ?? []) as FormatParser<any>[]
)
);

result[key] = value;
result[key] = value;

return result;
}, {} as FormatParsersPerCategory);
return result;
},
{
text: [] as TextFormatParser[],
} as FormatParsersPerCategory
);

additionalParsersArray.forEach(parsers => {
if (parsers?.text) {
result.text = result.text.concat(parsers.text);
}
});

return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ensureParagraph } from '../../modelApi/common/ensureParagraph';
import { getRegularSelectionOffsets } from '../utils/getRegularSelectionOffsets';
import { hasSpacesOnly } from '../../modelApi/common/hasSpacesOnly';
import { isWhiteSpacePreserved } from '../../domUtils/isWhiteSpacePreserved';
import { stackFormat } from '../utils/stackFormat';
import type {
ContentModelBlockGroup,
ContentModelParagraph,
Expand All @@ -22,6 +23,23 @@ export const textProcessor: ElementProcessor<Text> = (
textNode: Text,
context: DomToModelContext
) => {
if (context.formatParsers.text.length > 0) {
stackFormat(context, { segment: 'shallowClone' }, () => {
context.formatParsers.text.forEach(parser => {
parser(context.segmentFormat, textNode, context);
internalTextProcessor(group, textNode, context);
});
});
} else {
internalTextProcessor(group, textNode, context);
}
};

function internalTextProcessor(
group: ContentModelBlockGroup,
textNode: Text,
context: DomToModelContext
) {
let txt = textNode.nodeValue || '';
const offsets = getRegularSelectionOffsets(context, textNode);
const txtStartOffset = offsets[0];
Expand Down Expand Up @@ -61,7 +79,7 @@ export const textProcessor: ElementProcessor<Text> = (
paragraph,
segments.filter((x): x is ContentModelText => !!x)
);
};
}

function addTextSegment(
group: ContentModelBlockGroup,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
ModelToDomOption,
ModelToDomSelectionContext,
ModelToDomSettings,
TextFormatApplier,
} from 'roosterjs-content-model-types';

/**
Expand Down Expand Up @@ -100,22 +101,35 @@ export function buildFormatAppliers(
): FormatAppliersPerCategory {
const combinedOverrides = Object.assign({}, ...overrides);

return getObjectKeys(defaultFormatKeysPerCategory).reduce((result, key) => {
const value = defaultFormatKeysPerCategory[key]
.map(
formatKey =>
(combinedOverrides[formatKey] === undefined
? defaultFormatAppliers[formatKey]
: combinedOverrides[formatKey]) as FormatApplier<any>
)
.concat(
...additionalAppliersArray.map(
appliers => (appliers?.[key] ?? []) as FormatApplier<any>[]
const result = getObjectKeys(defaultFormatKeysPerCategory).reduce(
(result, key) => {
const value = defaultFormatKeysPerCategory[key]
.map(
formatKey =>
(combinedOverrides[formatKey] === undefined
? defaultFormatAppliers[formatKey]
: combinedOverrides[formatKey]) as FormatApplier<any>
)
);
.concat(
...additionalAppliersArray.map(
appliers => (appliers?.[key] ?? []) as FormatApplier<any>[]
)
);

result[key] = value;
result[key] = value;

return result;
}, {} as FormatAppliersPerCategory);
return result;
},
{
text: [] as TextFormatApplier[],
} as FormatAppliersPerCategory
);

additionalAppliersArray.forEach(appliers => {
if (appliers?.text) {
result.text = result.text.concat(appliers.text);
}
});

return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ export const handleText: ContentModelSegmentHandler<ContentModelText> = (
parent.appendChild(element);
element.appendChild(txt);

context.formatAppliers.text.forEach(applier => applier(segment.format, txt, context));

handleSegmentCommon(doc, txt, element, segment, context, segmentNodes);
};
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,29 @@ describe('createDomToModelContext', () => {
const mockedAProcessor = 'a' as any;
const mockedBoldParser = 'bold' as any;
const mockedBlockParser = 'block' as any;
const context = createDomToModelContext(undefined, undefined, {
processorOverride: {
a: mockedAProcessor,
},
formatParserOverride: {
bold: mockedBoldParser,
const mockedTextParser1 = 'parser1' as any;
const mockedTextParser2 = 'parser2' as any;
const context = createDomToModelContext(
undefined,
undefined,
{
processorOverride: {
a: mockedAProcessor,
},
formatParserOverride: {
bold: mockedBoldParser,
},
additionalFormatParsers: {
block: mockedBlockParser,
text: [mockedTextParser1],
},
},
additionalFormatParsers: {
block: mockedBlockParser,
},
});
{
additionalFormatParsers: {
text: [mockedTextParser2],
},
}
);

const parsers = buildFormatParsers();

Expand All @@ -93,6 +105,7 @@ describe('createDomToModelContext', () => {
parsers.segment[7] = mockedBoldParser;
parsers.segmentOnBlock[7] = mockedBoldParser;
parsers.segmentOnTableCell[7] = mockedBoldParser;
parsers.text = [mockedTextParser1, mockedTextParser2];

expect(context).toEqual({
isInSelection: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -692,4 +692,39 @@ describe('textProcessor', () => {
});
expect(onSegmentSpy).toHaveBeenCalledWith(text, paragraph, [segment1, segment2, segment3]);
});

it('process with text format parser', () => {
const doc = createContentModelDocument();
const text = document.createTextNode('test1');
const parserSpy = jasmine.createSpy('parser');

context.formatParsers.text = [parserSpy];

doc.blocks.push({
blockType: 'Paragraph',
segments: [],
format: {},
});

textProcessor(doc, text, context);

expect(doc).toEqual({
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
segments: [
{
segmentType: 'Text',
text: 'test1',
format: {},
},
],
format: {},
},
],
});
expect(parserSpy).toHaveBeenCalledTimes(1);
expect(parserSpy).toHaveBeenCalledWith({}, text, context);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,28 @@ describe('createModelToDomContext', () => {
const mockedBoldApplier = 'bold' as any;
const mockedBlockApplier = 'block' as any;
const mockedBrHandler = 'br' as any;
const context = createModelToDomContext(undefined, {
modelHandlerOverride: {
br: mockedBrHandler,
},
formatApplierOverride: {
bold: mockedBoldApplier,
},
additionalFormatAppliers: {
block: [mockedBlockApplier],
const mockedTextApplier1 = 'applier1' as any;
const mockedTextApplier2 = 'applier2' as any;
const context = createModelToDomContext(
undefined,
{
modelHandlerOverride: {
br: mockedBrHandler,
},
formatApplierOverride: {
bold: mockedBoldApplier,
},
additionalFormatAppliers: {
block: [mockedBlockApplier],
text: [mockedTextApplier1],
},
},
});
{
additionalFormatAppliers: {
text: [mockedTextApplier2],
},
}
);

const appliers = buildFormatAppliers();

Expand All @@ -81,6 +92,7 @@ describe('createModelToDomContext', () => {
appliers.segment[7] = mockedBoldApplier;
appliers.segmentOnBlock[7] = mockedBoldApplier;
appliers.segmentOnTableCell[7] = mockedBoldApplier;
appliers.text = [mockedTextApplier1, mockedTextApplier2];

expect(context).toEqual({
regularSelection: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,24 @@ describe('handleText', () => {
expect(segmentNodes.length).toBe(1);
expect(segmentNodes[0]).toBe(parent.firstChild!.firstChild!);
});

it('Handle text with format applier', () => {
const text: ContentModelText = {
segmentType: 'Text',
text: 'test',
format: {},
};
const segmentNodes: Node[] = [];
const applierSpy = jasmine.createSpy('applier');

context.formatAppliers.text = [applierSpy];

handleText(document, parent, text, context, segmentNodes);

expect(parent.innerHTML).toBe('<span>test</span>');
expect(segmentNodes.length).toBe(1);
expect(segmentNodes[0]).toBe(parent.firstChild!.firstChild!);
expect(applierSpy).toHaveBeenCalledTimes(1);
expect(applierSpy).toHaveBeenCalledWith(text.format, segmentNodes[0], context);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import type {
ContentModelFormatMap,
DomToModelOption,
FormatParser,
FormatParsersPerCategory,
ElementFormatParserPerCategory,
} from 'roosterjs-content-model-types';

/**
* @internal
*/
export default function addParser<TKey extends keyof FormatParsersPerCategory>(
export default function addParser<TKey extends keyof ElementFormatParserPerCategory>(
domToModelOption: DomToModelOption,
entry: TKey,
additionalFormatParsers: FormatParser<ContentModelFormatMap[TKey]>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat';
import type { ContentModelFormatBase } from '../format/ContentModelFormatBase';
import type { ContentModelFormatMap } from '../format/ContentModelFormatMap';
import type { DomToModelContext } from './DomToModelContext';
Expand Down Expand Up @@ -32,6 +33,16 @@ export type FormatParser<TFormat extends ContentModelFormatBase> = (
defaultStyle: Readonly<Partial<CSSStyleDeclaration>>
) => void;

/**
* Parse format from the given text node
* @param format The format object to parse into
* @param textNode The text node to parse format from
* @param context The context object that provide related context information
*/
export type TextFormatParser<
TFormat extends ContentModelSegmentFormat = ContentModelSegmentFormat
> = (format: TFormat, textNode: Text, context: DomToModelContext) => void;

/**
* All format parsers
*/
Expand All @@ -40,12 +51,19 @@ export type FormatParsers = {
};

/**
* A map from format parser category name to an array of parsers
* A map from format parser category name to an array of parsers. This is for HTML Element only
*/
export type FormatParsersPerCategory = {
export type ElementFormatParserPerCategory = {
[Key in keyof ContentModelFormatMap]: (FormatParser<ContentModelFormatMap[Key]> | null)[];
};

/**
* A map from format parser category name to an array of parsers
*/
export type FormatParsersPerCategory = ElementFormatParserPerCategory & {
text: TextFormatParser[];
};

/**
* A map from element processor name to its processor type
*/
Expand Down
Loading

0 comments on commit 9d98823

Please sign in to comment.