From 3d74e90f01a80b88ed113d2c2c363a0808994fff Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 29 Nov 2022 12:19:57 -0800 Subject: [PATCH] Content Model Support PRE and CODE: step 1 (#1439) * Support PRE and CODE: step 1 * improve --- .gitignore | 1 - .../paragraph/marginFormatHandler.ts | 8 +- .../segment/boldFormatHandler.ts | 2 +- .../segment/fontFamilyFormatHandler.ts | 2 +- .../segment/fontSizeFormatHandler.ts | 2 +- .../segment/italicFormatHandler.ts | 2 +- .../segment/textColorFormatHandler.ts | 2 +- .../segment/underlineFormatHandler.ts | 2 +- .../lib/formatHandlers/utils/defaultStyles.ts | 4 +- packages/roosterjs-content-model/lib/index.ts | 2 +- .../context/createModelToDomContext.ts | 10 +- .../modelToDom/handlers/handleGeneralModel.ts | 15 +-- .../lib/modelToDom/handlers/handleImage.ts | 15 +-- .../modelToDom/handlers/handleParagraph.ts | 72 ++++++------ .../lib/modelToDom/handlers/handleText.ts | 15 +-- .../lib/modelToDom/utils/stackFormat.ts | 29 +++++ .../lib/publicApi/block/setHeaderLevel.ts | 10 +- .../IExperimentalContentModelEditor.ts | 4 +- .../context/ModelToDomFormatContext.ts | 5 +- .../publicTypes/context/ModelToDomSettings.ts | 10 +- .../context/createModelToDomContextTest.ts | 12 +- .../paragraph/marginFormatHandlerTest.ts | 106 ++++++++++++++++++ .../segment/boldFormatHandlerTest.ts | 6 +- .../segment/fontFamilyFormatHandlerTest.ts | 6 +- .../segment/fontSizeFormatHandlerTest.ts | 6 +- .../segment/underlineFormatHandlerTest.ts | 4 +- .../handlers/handleGeneralModelTest.ts | 24 ++++ .../modelToDom/handlers/handleImageTest.ts | 18 +++ .../handlers/handleParagraphTest.ts | 31 +++++ .../modelToDom/handlers/handleTextTest.ts | 18 +++ .../test/modelToDom/utils/stackFormatTest.ts | 57 ++++++++++ 31 files changed, 380 insertions(+), 120 deletions(-) create mode 100644 packages/roosterjs-content-model/lib/modelToDom/utils/stackFormat.ts create mode 100644 packages/roosterjs-content-model/test/formatHandlers/paragraph/marginFormatHandlerTest.ts create mode 100644 packages/roosterjs-content-model/test/modelToDom/utils/stackFormatTest.ts diff --git a/.gitignore b/.gitignore index fd8f2e1583e..bec590a8490 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,3 @@ dist/ # Temp files packages/roosterjs-editor-types/lib/compatibleEnum/ -packages/roosterjs-content-model/lib/publicTypes/compatibleEnum/ diff --git a/packages/roosterjs-content-model/lib/formatHandlers/paragraph/marginFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/paragraph/marginFormatHandler.ts index 9bb72d6c2a5..451dee7dd5f 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/paragraph/marginFormatHandler.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/paragraph/marginFormatHandler.ts @@ -12,7 +12,7 @@ const MarginKeys: (keyof MarginFormat & keyof CSSStyleDeclaration)[] = [ * @internal */ export const marginFormatHandler: FormatHandler = { - parse: (format, element, context, defaultStyle) => { + parse: (format, element, _, defaultStyle) => { MarginKeys.forEach(key => { const value = element.style[key] || defaultStyle[key]; @@ -21,12 +21,12 @@ export const marginFormatHandler: FormatHandler = { } }); }, - apply: (format, element) => { + apply: (format, element, context) => { MarginKeys.forEach(key => { const value = format[key]; - if (value) { - element.style[key] = value; + if (value != context.implicitFormat[key]) { + element.style[key] = value || '0'; } }); }, diff --git a/packages/roosterjs-content-model/lib/formatHandlers/segment/boldFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/segment/boldFormatHandler.ts index 06e01f7b5a2..6957420ed94 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/segment/boldFormatHandler.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/segment/boldFormatHandler.ts @@ -14,7 +14,7 @@ export const boldFormatHandler: FormatHandler = { } }, apply: (format, element, context) => { - const blockFontWeight = context.implicitSegmentFormat.fontWeight; + const blockFontWeight = context.implicitFormat.fontWeight; if ( (blockFontWeight && blockFontWeight != format.fontWeight) || diff --git a/packages/roosterjs-content-model/lib/formatHandlers/segment/fontFamilyFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/segment/fontFamilyFormatHandler.ts index c7bd8afa49d..f67ec0eb02f 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/segment/fontFamilyFormatHandler.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/segment/fontFamilyFormatHandler.ts @@ -13,7 +13,7 @@ export const fontFamilyFormatHandler: FormatHandler = { } }, apply: (format, element, context) => { - if (format.fontFamily && format.fontFamily != context.implicitSegmentFormat.fontFamily) { + if (format.fontFamily && format.fontFamily != context.implicitFormat.fontFamily) { element.style.fontFamily = format.fontFamily; } }, diff --git a/packages/roosterjs-content-model/lib/formatHandlers/segment/fontSizeFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/segment/fontSizeFormatHandler.ts index b53ea4f96bd..1ba5dbc105f 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/segment/fontSizeFormatHandler.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/segment/fontSizeFormatHandler.ts @@ -17,7 +17,7 @@ export const fontSizeFormatHandler: FormatHandler = { } }, apply: (format, element, context) => { - if (format.fontSize && format.fontSize != context.implicitSegmentFormat.fontSize) { + if (format.fontSize && format.fontSize != context.implicitFormat.fontSize) { element.style.fontSize = format.fontSize; } }, diff --git a/packages/roosterjs-content-model/lib/formatHandlers/segment/italicFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/segment/italicFormatHandler.ts index dc2d0893e42..89d6b2c11a9 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/segment/italicFormatHandler.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/segment/italicFormatHandler.ts @@ -16,7 +16,7 @@ export const italicFormatHandler: FormatHandler = { } }, apply: (format, element, context) => { - const implicitItalic = context.implicitSegmentFormat.italic; + const implicitItalic = context.implicitFormat.italic; if (!!implicitItalic != !!format.italic) { if (format.italic) { diff --git a/packages/roosterjs-content-model/lib/formatHandlers/segment/textColorFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/segment/textColorFormatHandler.ts index 9aa17196ffc..b740ff32a65 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/segment/textColorFormatHandler.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/segment/textColorFormatHandler.ts @@ -15,7 +15,7 @@ export const textColorFormatHandler: FormatHandler = { } }, apply: (format, element, context) => { - const implicitColor = context.implicitSegmentFormat.textColor; + const implicitColor = context.implicitFormat.textColor; if (format.textColor && format.textColor != implicitColor) { setColor( diff --git a/packages/roosterjs-content-model/lib/formatHandlers/segment/underlineFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/segment/underlineFormatHandler.ts index 448f2e338ea..1cbe3b8733c 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/segment/underlineFormatHandler.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/segment/underlineFormatHandler.ts @@ -16,7 +16,7 @@ export const underlineFormatHandler: FormatHandler = { } }, apply: (format, element, context) => { - const blockUnderline = context.implicitSegmentFormat.underline; + const blockUnderline = context.implicitFormat.underline; if (!!blockUnderline != !!format.underline) { if (format.underline) { diff --git a/packages/roosterjs-content-model/lib/formatHandlers/utils/defaultStyles.ts b/packages/roosterjs-content-model/lib/formatHandlers/utils/defaultStyles.ts index 0231d8b95b3..66545db6b29 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/utils/defaultStyles.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/utils/defaultStyles.ts @@ -1,4 +1,4 @@ -import { DefaultImplicitSegmentFormatMap } from '../../publicTypes/context/ModelToDomSettings'; +import { DefaultImplicitFormatMap } from '../../publicTypes/context/ModelToDomSettings'; import { DefaultStyleMap } from '../../publicTypes/context/DomToModelSettings'; const blockElement: Partial = { @@ -131,7 +131,7 @@ export const defaultStyleMap: DefaultStyleMap = { ul: blockElement, }; -export const defaultImplicitSegmentFormatMap: DefaultImplicitSegmentFormatMap = { +export const defaultImplicitFormatMap: DefaultImplicitFormatMap = { a: { underline: true, textColor: HyperLinkColorPlaceholder, diff --git a/packages/roosterjs-content-model/lib/index.ts b/packages/roosterjs-content-model/lib/index.ts index 3d223e3f9e0..a656770d80f 100644 --- a/packages/roosterjs-content-model/lib/index.ts +++ b/packages/roosterjs-content-model/lib/index.ts @@ -142,7 +142,7 @@ export { FormatAppliersPerCategory, ContentModelHandlerMap, ContentModelHandlerTypeMap, - DefaultImplicitSegmentFormatMap, + DefaultImplicitFormatMap, } from './publicTypes/context/ModelToDomSettings'; export { ModelToDomEntityContext } from './publicTypes/context/ModelToDomEntityContext'; export { ElementProcessor } from './publicTypes/context/ElementProcessor'; diff --git a/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts b/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts index 6fc876eab9d..88275067818 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts @@ -1,5 +1,5 @@ import { defaultContentModelHandlers } from './defaultContentModelHandlers'; -import { defaultImplicitSegmentFormatMap } from '../../formatHandlers/utils/defaultStyles'; +import { defaultImplicitFormatMap } from '../../formatHandlers/utils/defaultStyles'; import { EditorContext } from '../../publicTypes/context/EditorContext'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; import { ModelToDomOption } from '../../publicTypes/IExperimentalContentModelEditor'; @@ -34,7 +34,7 @@ export function createModelToDomContext( threadItemCounts: [], nodeStack: [], }, - implicitSegmentFormat: {}, + implicitFormat: {}, formatAppliers: getFormatAppliers( options?.formatApplierOverride, options?.additionalFormatAppliers @@ -43,9 +43,9 @@ export function createModelToDomContext( ...defaultContentModelHandlers, ...(options?.modelHandlerOverride || {}), }, - defaultImplicitSegmentFormatMap: { - ...defaultImplicitSegmentFormatMap, - ...(options?.defaultImplicitSegmentFormatOverride || {}), + defaultImplicitFormatMap: { + ...defaultImplicitFormatMap, + ...(options?.defaultImplicitFormatOverride || {}), }, entities: {}, diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts index 2e44b3ffb9d..8a678ff85c5 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts @@ -5,6 +5,7 @@ import { ContentModelHandler } from '../../publicTypes/context/ContentModelHandl import { isNodeOfType } from '../../domUtils/isNodeOfType'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; import { NodeType } from 'roosterjs-editor-types'; +import { stackFormat } from '../utils/stackFormat'; /** * @internal @@ -22,21 +23,15 @@ export const handleGeneralModel: ContentModelHandler = context.regularSelection.current.segment = newParent; } - const implicitSegmentFormat = context.implicitSegmentFormat; - let segmentElement: HTMLElement; + stackFormat(context, group.link ? 'a' : null, () => { + let segmentElement: HTMLElement; - try { if (group.link) { segmentElement = doc.createElement('a'); parent.appendChild(segmentElement); segmentElement.appendChild(newParent); - context.implicitSegmentFormat = { - ...implicitSegmentFormat, - ...(context.defaultImplicitSegmentFormatMap.a || {}), - }; - applyFormat( segmentElement, context.formatAppliers.link, @@ -55,9 +50,7 @@ export const handleGeneralModel: ContentModelHandler = } applyFormat(segmentElement, context.formatAppliers.segment, group.format, context); - } finally { - context.implicitSegmentFormat = implicitSegmentFormat; - } + }); } else { parent.appendChild(newParent); } diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleImage.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleImage.ts index c023eb6448c..f20305a0ebe 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleImage.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleImage.ts @@ -2,6 +2,7 @@ import { applyFormat } from '../utils/applyFormat'; import { ContentModelHandler } from '../../publicTypes/context/ContentModelHandler'; import { ContentModelImage } from '../../publicTypes/segment/ContentModelImage'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; +import { stackFormat } from '../utils/stackFormat'; /** * @internal @@ -23,21 +24,15 @@ export const handleImage: ContentModelHandler = ( img.title = imageModel.title; } - const implicitSegmentFormat = context.implicitSegmentFormat; - let segmentElement: HTMLElement; + stackFormat(context, imageModel.link ? 'a' : null, () => { + let segmentElement: HTMLElement; - try { if (imageModel.link) { segmentElement = doc.createElement('a'); parent.appendChild(segmentElement); segmentElement.appendChild(img); - context.implicitSegmentFormat = { - ...implicitSegmentFormat, - ...(context.defaultImplicitSegmentFormatMap.a || {}), - }; - applyFormat( segmentElement, context.formatAppliers.link, @@ -58,9 +53,7 @@ export const handleImage: ContentModelHandler = ( applyFormat(img, context.formatAppliers.image, imageModel.format, context); applyFormat(segmentElement, context.formatAppliers.segment, imageModel.format, context); applyFormat(img, context.formatAppliers.dataset, imageModel.dataset, context); - } finally { - context.implicitSegmentFormat = implicitSegmentFormat; - } + }); context.regularSelection.current.segment = img; diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleParagraph.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleParagraph.ts index 6f87f795b46..ca151add6f1 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleParagraph.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleParagraph.ts @@ -3,6 +3,7 @@ import { ContentModelHandler } from '../../publicTypes/context/ContentModelHandl import { ContentModelParagraph } from '../../publicTypes/block/ContentModelParagraph'; import { getObjectKeys } from 'roosterjs-editor-dom'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; +import { stackFormat } from '../utils/stackFormat'; /** * @internal @@ -14,50 +15,41 @@ export const handleParagraph: ContentModelHandler = ( context: ModelToDomContext ) => { let container: HTMLElement; - const implicitSegmentFormat = context.implicitSegmentFormat; - if (paragraph.header) { - const tag = ('h' + paragraph.header.headerLevel) as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; - - container = doc.createElement(tag); - parent.appendChild(container); - context.implicitSegmentFormat = { - ...implicitSegmentFormat, - ...(context.defaultImplicitSegmentFormatMap[tag] || {}), + stackFormat(context, paragraph.header ? 'h' + paragraph.header.headerLevel : null, () => { + if (paragraph.header) { + const tag = 'h' + paragraph.header.headerLevel; + + container = doc.createElement(tag); + parent.appendChild(container); + + applyFormat(container, context.formatAppliers.block, paragraph.format, context); + applyFormat( + container, + context.formatAppliers.segmentOnBlock, + paragraph.header.format, + context + ); + } else if ( + !paragraph.isImplicit || + (getObjectKeys(paragraph.format).length > 0 && + paragraph.segments.some(segment => segment.segmentType != 'SelectionMarker')) + ) { + container = doc.createElement('div'); + parent.appendChild(container); + + applyFormat(container, context.formatAppliers.block, paragraph.format, context); + } else { + container = parent as HTMLElement; + } + + context.regularSelection.current = { + block: container, + segment: null, }; - applyFormat(container, context.formatAppliers.block, paragraph.format, context); - applyFormat( - container, - context.formatAppliers.segmentOnBlock, - paragraph.header.format, - context - ); - - Object.assign(context.implicitSegmentFormat, paragraph.header.format); - } else if ( - !paragraph.isImplicit || - (getObjectKeys(paragraph.format).length > 0 && - paragraph.segments.some(segment => segment.segmentType != 'SelectionMarker')) - ) { - container = doc.createElement('div'); - parent.appendChild(container); - - applyFormat(container, context.formatAppliers.block, paragraph.format, context); - } else { - container = parent as HTMLElement; - } - - context.regularSelection.current = { - block: container, - segment: null, - }; - - try { paragraph.segments.forEach(segment => { context.modelHandlers.segment(doc, container, segment, context); }); - } finally { - context.implicitSegmentFormat = implicitSegmentFormat; - } + }); }; diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts index 0c3801f09cf..0203fdb8ae9 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts @@ -2,6 +2,7 @@ import { applyFormat } from '../utils/applyFormat'; import { ContentModelHandler } from '../../publicTypes/context/ContentModelHandler'; import { ContentModelText } from '../../publicTypes/segment/ContentModelText'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; +import { stackFormat } from '../utils/stackFormat'; /** * @internal @@ -13,7 +14,6 @@ export const handleText: ContentModelHandler = ( context: ModelToDomContext ) => { const txt = doc.createTextNode(segment.text); - const implicitSegmentFormat = context.implicitSegmentFormat; const element = doc.createElement(segment.link ? 'a' : 'span'); element.appendChild(txt); @@ -21,21 +21,12 @@ export const handleText: ContentModelHandler = ( context.regularSelection.current.segment = txt; - if (segment.link) { - context.implicitSegmentFormat = { - ...implicitSegmentFormat, - ...(context.defaultImplicitSegmentFormatMap.a || {}), - }; - } - - try { + stackFormat(context, segment.link ? 'a' : null, () => { applyFormat(element, context.formatAppliers.segment, segment.format, context); if (segment.link) { applyFormat(element, context.formatAppliers.link, segment.link.format, context); applyFormat(element, context.formatAppliers.dataset, segment.link.dataset, context); } - } finally { - context.implicitSegmentFormat = implicitSegmentFormat; - } + }); }; diff --git a/packages/roosterjs-content-model/lib/modelToDom/utils/stackFormat.ts b/packages/roosterjs-content-model/lib/modelToDom/utils/stackFormat.ts new file mode 100644 index 00000000000..211243e9480 --- /dev/null +++ b/packages/roosterjs-content-model/lib/modelToDom/utils/stackFormat.ts @@ -0,0 +1,29 @@ +import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; + +/** + * @internal + */ +export function stackFormat( + context: ModelToDomContext, + tagName: string | null, + callback: () => void +) { + if (tagName) { + const implicitFormat = context.implicitFormat; + + try { + const newFormat = context.defaultImplicitFormatMap[tagName] || {}; + + context.implicitFormat = { + ...implicitFormat, + ...newFormat, + }; + + callback(); + } finally { + context.implicitFormat = implicitFormat; + } + } else { + callback(); + } +} diff --git a/packages/roosterjs-content-model/lib/publicApi/block/setHeaderLevel.ts b/packages/roosterjs-content-model/lib/publicApi/block/setHeaderLevel.ts index 92cdd0b4900..6513f2ceed3 100644 --- a/packages/roosterjs-content-model/lib/publicApi/block/setHeaderLevel.ts +++ b/packages/roosterjs-content-model/lib/publicApi/block/setHeaderLevel.ts @@ -1,4 +1,5 @@ -import { defaultImplicitSegmentFormatMap } from '../../formatHandlers/utils/defaultStyles'; +import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat'; +import { defaultImplicitFormatMap } from '../../formatHandlers/utils/defaultStyles'; import { formatParagraphWithContentModel } from '../utils/formatParagraphWithContentModel'; import { getObjectKeys } from 'roosterjs-editor-dom'; import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; @@ -20,7 +21,8 @@ export default function setHeaderLevel( : para.header && para.header.headerLevel > 0 ? 'h' + para.header.headerLevel : null) as HeaderLevelTags | null; - const headerStyle = (tag && defaultImplicitSegmentFormatMap[tag]) || {}; + const headerStyle = + ((tag && defaultImplicitFormatMap[tag]) as ContentModelSegmentFormat) || {}; if (headerLevel > 0) { para.header = { @@ -34,8 +36,10 @@ export default function setHeaderLevel( } else { delete para.header; + const headerStyleKeys = getObjectKeys(headerStyle); + para.segments.forEach(segment => { - getObjectKeys(headerStyle).forEach(key => { + headerStyleKeys.forEach(key => { delete segment.format[key]; }); }); diff --git a/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts b/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts index 5f9ddab458d..e68b02e443b 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts @@ -3,7 +3,7 @@ import { EditorContext } from './context/EditorContext'; import { IEditor, SelectionRangeEx } from 'roosterjs-editor-types'; import { ContentModelHandlerMap, - DefaultImplicitSegmentFormatMap, + DefaultImplicitFormatMap, FormatAppliers, FormatAppliersPerCategory, } from './context/ModelToDomSettings'; @@ -95,7 +95,7 @@ export interface ModelToDomOption { /** * Overrides default element styles */ - defaultImplicitSegmentFormatOverride?: DefaultImplicitSegmentFormatMap; + defaultImplicitFormatOverride?: DefaultImplicitFormatMap; } /** diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomFormatContext.ts b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomFormatContext.ts index 62b299b7a78..9d8db0356ed 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomFormatContext.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomFormatContext.ts @@ -1,3 +1,4 @@ +import { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import { ContentModelListItemLevelFormat } from '../format/ContentModelListItemLevelFormat'; import { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; @@ -36,7 +37,7 @@ export interface ModelToDomFormatContext { listFormat: ModelToDomListContext; /** - * Existing segment format implicitly applied from parent element + * Existing format implicitly applied from parent element */ - implicitSegmentFormat: ContentModelSegmentFormat; + implicitFormat: ContentModelSegmentFormat & ContentModelBlockFormat; } diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts index 433931fbd9a..891c445691b 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts @@ -1,4 +1,5 @@ import { ContentModelBlock } from '../block/ContentModelBlock'; +import { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import { ContentModelBlockGroup } from '../group/ContentModelBlockGroup'; import { ContentModelBr } from '../segment/ContentModelBr'; import { ContentModelEntity } from '../entity/ContentModelEntity'; @@ -19,9 +20,12 @@ import { FormatHandlerTypeMap, FormatKey } from '../format/FormatHandlerTypeMap' import { ModelToDomContext } from './ModelToDomContext'; /** - * Default implicit format map from tag name (lower case) to segment fromat + * Default implicit format map from tag name (lower case) to segment format */ -export type DefaultImplicitSegmentFormatMap = Record>; +export type DefaultImplicitFormatMap = Record< + string, + Readonly +>; /** * Apply format to the given HTML element @@ -153,7 +157,7 @@ export interface ModelToDomSettings { /** * Map of default implicit format for segment */ - defaultImplicitSegmentFormatMap: DefaultImplicitSegmentFormatMap; + defaultImplicitFormatMap: DefaultImplicitFormatMap; /** * Default Content Model to DOM handlers before overriding. diff --git a/packages/roosterjs-content-model/test/domToModel/context/createModelToDomContextTest.ts b/packages/roosterjs-content-model/test/domToModel/context/createModelToDomContextTest.ts index fa9f279a240..761bf479e19 100644 --- a/packages/roosterjs-content-model/test/domToModel/context/createModelToDomContextTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/context/createModelToDomContextTest.ts @@ -1,6 +1,6 @@ import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; import { defaultContentModelHandlers } from '../../../lib/modelToDom/context/defaultContentModelHandlers'; -import { defaultImplicitSegmentFormatMap } from '../../../lib/formatHandlers/utils/defaultStyles'; +import { defaultImplicitFormatMap } from '../../../lib/formatHandlers/utils/defaultStyles'; import { EditorContext } from '../../../lib/publicTypes/context/EditorContext'; import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; import { @@ -27,10 +27,10 @@ describe('createModelToDomContext', () => { threadItemCounts: [], nodeStack: [], }, - implicitSegmentFormat: {}, + implicitFormat: {}, formatAppliers: getFormatAppliers(), modelHandlers: defaultContentModelHandlers, - defaultImplicitSegmentFormatMap: defaultImplicitSegmentFormatMap, + defaultImplicitFormatMap: defaultImplicitFormatMap, entities: {}, defaultModelHandlers: defaultContentModelHandlers, defaultFormatAppliers: defaultFormatAppliers, @@ -75,7 +75,7 @@ describe('createModelToDomContext', () => { modelHandlerOverride: { br: mockedBrHandler, }, - defaultImplicitSegmentFormatOverride: { + defaultImplicitFormatOverride: { a: mockedAStyle, }, }); @@ -90,13 +90,13 @@ describe('createModelToDomContext', () => { threadItemCounts: [], nodeStack: [], }); - expect(context.implicitSegmentFormat).toEqual({}); + expect(context.implicitFormat).toEqual({}); expect(context.formatAppliers.block).toEqual([ ...getFormatAppliers().block, mockedBlockApplier, ]); expect(context.modelHandlers.br).toBe(mockedBrHandler); - expect(context.defaultImplicitSegmentFormatMap.a).toEqual(mockedAStyle); + expect(context.defaultImplicitFormatMap.a).toEqual(mockedAStyle); expect(context.entities).toEqual({}); expect(context.defaultModelHandlers).toEqual(defaultContentModelHandlers); expect(context.defaultFormatAppliers).toEqual(defaultFormatAppliers); diff --git a/packages/roosterjs-content-model/test/formatHandlers/paragraph/marginFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/paragraph/marginFormatHandlerTest.ts new file mode 100644 index 00000000000..ef0ff1e6baa --- /dev/null +++ b/packages/roosterjs-content-model/test/formatHandlers/paragraph/marginFormatHandlerTest.ts @@ -0,0 +1,106 @@ +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { MarginFormat } from '../../../lib/publicTypes/format/formatParts/MarginFormat'; +import { marginFormatHandler } from '../../../lib/formatHandlers/paragraph/marginFormatHandler'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; + +describe('marginFormatHandler.parse', () => { + let div: HTMLElement; + let format: MarginFormat; + let context: DomToModelContext; + + beforeEach(() => { + div = document.createElement('div'); + format = {}; + context = createDomToModelContext(); + }); + + it('No margin', () => { + marginFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({}); + }); + + it('Has margin in CSS', () => { + div.style.margin = '1px 2px 3px 4px'; + marginFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({ + marginTop: '1px', + marginRight: '2px', + marginBottom: '3px', + marginLeft: '4px', + }); + }); + + it('Has margin in default style', () => { + marginFormatHandler.parse(format, div, context, { + marginTop: '1em', + marginBottom: '1em', + }); + expect(format).toEqual({ + marginTop: '1em', + marginBottom: '1em', + }); + }); +}); + +describe('marginFormatHandler.apply', () => { + let div: HTMLElement; + let format: MarginFormat; + let context: ModelToDomContext; + + beforeEach(() => { + div = document.createElement('div'); + format = {}; + context = createModelToDomContext(); + }); + + it('No margin', () => { + marginFormatHandler.apply(format, div, context); + expect(div.outerHTML).toBe('
'); + }); + + it('Has margin', () => { + format.marginTop = '1px'; + format.marginRight = '2px'; + format.marginBottom = '3px'; + format.marginLeft = '4px'; + + marginFormatHandler.apply(format, div, context); + + expect(div.outerHTML).toBe('
'); + }); + + it('Has implicit format', () => { + context.implicitFormat = { + marginTop: '1em', + marginBottom: '1em', + }; + + marginFormatHandler.apply(format, div, context); + expect(div.outerHTML).toBe('
'); + }); + + it('Has implicit format and different value in CSS', () => { + context.implicitFormat = { + marginTop: '1em', + marginBottom: '1em', + }; + format.marginTop = '2em'; + + marginFormatHandler.apply(format, div, context); + expect(div.outerHTML).toBe('
'); + }); + + it('Has implicit format and same value in CSS', () => { + context.implicitFormat = { + marginTop: '1em', + marginBottom: '1em', + }; + format.marginTop = '1em'; + format.marginBottom = '1em'; + + marginFormatHandler.apply(format, div, context); + expect(div.outerHTML).toBe('
'); + }); +}); diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/boldFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/boldFormatHandlerTest.ts index a3363240829..9c38f64486b 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/boldFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/boldFormatHandlerTest.ts @@ -130,7 +130,7 @@ describe('boldFormatHandler.apply', () => { it('Turn off bold when there is bold from block', () => { div.innerHTML = 'test'; - context.implicitSegmentFormat.fontWeight = 'bold'; + context.implicitFormat.fontWeight = 'bold'; boldFormatHandler.apply(format, div, context); @@ -139,7 +139,7 @@ describe('boldFormatHandler.apply', () => { it('Change bold when there is bold from block', () => { div.innerHTML = 'test'; - context.implicitSegmentFormat.fontWeight = 'bold'; + context.implicitFormat.fontWeight = 'bold'; format.fontWeight = '600'; boldFormatHandler.apply(format, div, context); @@ -149,7 +149,7 @@ describe('boldFormatHandler.apply', () => { it('No change when bold from block and same with current format', () => { div.innerHTML = 'test'; - context.implicitSegmentFormat.fontWeight = 'bold'; + context.implicitFormat.fontWeight = 'bold'; format.fontWeight = 'bold'; boldFormatHandler.apply(format, div, context); diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/fontFamilyFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/fontFamilyFormatHandlerTest.ts index 4c49443b805..9b8d48a36e2 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/fontFamilyFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/fontFamilyFormatHandlerTest.ts @@ -77,7 +77,7 @@ describe('fontFamilyFormatHandler.apply', () => { }); it('Has implicit font family from context', () => { - context.implicitSegmentFormat.fontFamily = 'test'; + context.implicitFormat.fontFamily = 'test'; fontFamilyFormatHandler.apply(format, div, context); @@ -85,7 +85,7 @@ describe('fontFamilyFormatHandler.apply', () => { }); it('Has implicit font family from context and same with current format', () => { - context.implicitSegmentFormat.fontFamily = 'test'; + context.implicitFormat.fontFamily = 'test'; format.fontFamily = 'test'; fontFamilyFormatHandler.apply(format, div, context); @@ -94,7 +94,7 @@ describe('fontFamilyFormatHandler.apply', () => { }); it('Has implicit font family from context but overridden by current format', () => { - context.implicitSegmentFormat.fontFamily = 'test'; + context.implicitFormat.fontFamily = 'test'; format.fontFamily = 'test2'; fontFamilyFormatHandler.apply(format, div, context); diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/fontSizeFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/fontSizeFormatHandlerTest.ts index 445929c475d..06c646d14d9 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/fontSizeFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/fontSizeFormatHandlerTest.ts @@ -85,7 +85,7 @@ describe('fontSizeFormatHandler.apply', () => { }); it('Has implicit font size from context', () => { - context.implicitSegmentFormat.fontSize = '20px'; + context.implicitFormat.fontSize = '20px'; fontSizeFormatHandler.apply(format, div, context); @@ -93,7 +93,7 @@ describe('fontSizeFormatHandler.apply', () => { }); it('Has implicit font size from context and same with current format', () => { - context.implicitSegmentFormat.fontSize = '20px'; + context.implicitFormat.fontSize = '20px'; format.fontSize = '20px'; fontSizeFormatHandler.apply(format, div, context); @@ -102,7 +102,7 @@ describe('fontSizeFormatHandler.apply', () => { }); it('Has implicit font size from context but overridden by current format', () => { - context.implicitSegmentFormat.fontSize = '20px'; + context.implicitFormat.fontSize = '20px'; format.fontSize = '40px'; fontSizeFormatHandler.apply(format, div, context); diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/underlineFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/underlineFormatHandlerTest.ts index 5a9dc8e4731..25fe7e850d4 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/underlineFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/underlineFormatHandlerTest.ts @@ -149,7 +149,7 @@ describe('underlineFormatHandler.apply', () => { a.textContent = 'test'; format.underline = true; - context.implicitSegmentFormat.underline = true; + context.implicitFormat.underline = true; underlineFormatHandler.apply(format, a, context); @@ -161,7 +161,7 @@ describe('underlineFormatHandler.apply', () => { a.textContent = 'test'; - context.implicitSegmentFormat.underline = true; + context.implicitFormat.underline = true; underlineFormatHandler.apply(format, a, context); diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts index ca4399cebfa..55dd2a191fb 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts @@ -1,4 +1,5 @@ import * as applyFormat from '../../../lib/modelToDom/utils/applyFormat'; +import * as stackFormat from '../../../lib/modelToDom/utils/stackFormat'; import { ContentModelBlockGroup } from '../../../lib/publicTypes/group/ContentModelBlockGroup'; import { ContentModelHandler } from '../../../lib/publicTypes/context/ContentModelHandler'; import { ContentModelListItem } from '../../../lib/publicTypes/group/ContentModelListItem'; @@ -142,4 +143,27 @@ describe('handleBlockGroup', () => { ); expect(applyFormat.applyFormat).toHaveBeenCalled(); }); + + it('call stackFormat', () => { + const clonedChild = document.createElement('span'); + const childMock = ({ + cloneNode: () => clonedChild, + firstChild: true, + } as any) as HTMLElement; + const group = createGeneralSegment(childMock, { underline: true }); + + group.link = { + format: { + href: '/test', + }, + dataset: {}, + }; + + spyOn(stackFormat, 'stackFormat').and.callThrough(); + + handleGeneralModel(document, parent, group, context); + + expect(stackFormat.stackFormat).toHaveBeenCalledTimes(1); + expect((stackFormat.stackFormat).calls.argsFor(0)[1]).toBe('a'); + }); }); diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleImageTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleImageTest.ts index fe5827c60c2..20502ae1383 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleImageTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleImageTest.ts @@ -1,3 +1,4 @@ +import * as stackFormat from '../../../lib/modelToDom/utils/stackFormat'; import { ContentModelBlock } from '../../../lib/publicTypes/block/ContentModelBlock'; import { ContentModelHandler } from '../../../lib/publicTypes/context/ContentModelHandler'; import { ContentModelImage } from '../../../lib/publicTypes/segment/ContentModelImage'; @@ -97,4 +98,21 @@ describe('handleSegment', () => { runTest(segment, '', 0); }); + + it('call stackFormat', () => { + const segment: ContentModelImage = { + segmentType: 'Image', + src: 'http://test.com/test', + format: { underline: true }, + link: { format: { href: '/test' }, dataset: {} }, + dataset: {}, + }; + + spyOn(stackFormat, 'stackFormat').and.callThrough(); + + runTest(segment, '', 0); + + expect(stackFormat.stackFormat).toHaveBeenCalledTimes(1); + expect((stackFormat.stackFormat).calls.argsFor(0)[1]).toBe('a'); + }); }); diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts index 3c913f93b20..2de3addc648 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts @@ -1,3 +1,4 @@ +import * as stackFormat from '../../../lib/modelToDom/utils/stackFormat'; import { ContentModelHandler } from '../../../lib/publicTypes/context/ContentModelHandler'; import { ContentModelParagraph } from '../../../lib/publicTypes/block/ContentModelParagraph'; import { ContentModelSegment } from '../../../lib/publicTypes/segment/ContentModelSegment'; @@ -289,4 +290,34 @@ describe('handleParagraph', () => { 1 ); }); + + it('call stackFormat', () => { + handleSegment.and.callFake(originalHandleSegment); + + spyOn(stackFormat, 'stackFormat').and.callThrough(); + + runTest( + { + blockType: 'Paragraph', + format: {}, + header: { + headerLevel: 1, + format: { fontWeight: 'bold', fontSize: '2em' }, + }, + segments: [ + { + segmentType: 'Text', + format: { fontWeight: 'bold' }, + text: 'test', + }, + ], + }, + '

test

', + 1 + ); + + expect(stackFormat.stackFormat).toHaveBeenCalledTimes(2); + expect((stackFormat.stackFormat).calls.argsFor(0)[1]).toBe('h1'); + expect((stackFormat.stackFormat).calls.argsFor(1)[1]).toBe(null); + }); }); diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts index 4bc151e4b87..38f8028573c 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts @@ -1,3 +1,4 @@ +import * as stackFormat from '../../../lib/modelToDom/utils/stackFormat'; import { ContentModelText } from '../../../lib/publicTypes/segment/ContentModelText'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; import { handleText } from '../../../lib/modelToDom/handlers/handleText'; @@ -48,4 +49,21 @@ describe('handleSegment', () => { expect(parent.innerHTML).toBe('test'); }); + + it('call stackFormat', () => { + const text: ContentModelText = { + segmentType: 'Text', + text: 'test', + format: { underline: true }, + link: { format: { href: '/test' }, dataset: {} }, + }; + + spyOn(stackFormat, 'stackFormat').and.callThrough(); + + handleText(document, parent, text, context); + + expect(parent.innerHTML).toBe('test'); + expect(stackFormat.stackFormat).toHaveBeenCalledTimes(1); + expect((stackFormat.stackFormat).calls.argsFor(0)[1]).toBe('a'); + }); }); diff --git a/packages/roosterjs-content-model/test/modelToDom/utils/stackFormatTest.ts b/packages/roosterjs-content-model/test/modelToDom/utils/stackFormatTest.ts new file mode 100644 index 00000000000..1564ab266e3 --- /dev/null +++ b/packages/roosterjs-content-model/test/modelToDom/utils/stackFormatTest.ts @@ -0,0 +1,57 @@ +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { stackFormat } from '../../../lib/modelToDom/utils/stackFormat'; + +describe('stackFormat', () => { + it('no tag', () => { + const context = createModelToDomContext(); + const format = { + fontSize: '10px', + }; + const callback = jasmine.createSpy().and.callFake(() => { + expect(context.implicitFormat).toBe(format); + }); + + context.implicitFormat = format; + + stackFormat(context, null, callback); + + expect(callback).toHaveBeenCalled(); + expect(context.implicitFormat).toEqual({ + fontSize: '10px', + }); + }); + + it('has a tag', () => { + const context = createModelToDomContext(); + const callback = jasmine.createSpy().and.callFake(() => { + expect(context.implicitFormat).toEqual({ + underline: true, + textColor: '__hyperLinkColor', + }); + context.implicitFormat.fontSize = '10px'; + }); + + stackFormat(context, 'a', callback); + + expect(callback).toHaveBeenCalled(); + expect(context.implicitFormat).toEqual({}); + }); + + it('has a tag and throw', () => { + const context = createModelToDomContext(); + const callback = jasmine.createSpy().and.callFake(() => { + expect(context.implicitFormat).toEqual({ + underline: true, + textColor: '__hyperLinkColor', + }); + context.implicitFormat.fontSize = '10px'; + throw new Error('test'); + }); + + const func = () => stackFormat(context, 'a', callback); + + expect(func).toThrow(); + expect(callback).toHaveBeenCalled(); + expect(context.implicitFormat).toEqual({}); + }); +});