Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content Model: Customization refactor step 2 #2050

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { DefaultContentModelFormatMap } from 'roosterjs-content-model-types';

/**
* @internal
* A map from tag name to its default implicit formats
*/
export const defaultContentModelFormatMap: DefaultContentModelFormatMap = {
a: {
underline: true,
},
blockquote: {
marginTop: '1em',
marginBottom: '1em',
marginLeft: '40px',
marginRight: '40px',
},
code: {
fontFamily: 'monospace',
},
h1: {
fontWeight: 'bold',
fontSize: '2em',
},
h2: {
fontWeight: 'bold',
fontSize: '1.5em',
},
h3: {
fontWeight: 'bold',
fontSize: '1.17em',
},
h4: {
fontWeight: 'bold',
fontSize: '1em', // Set this default value here to overwrite existing font size when change heading level
},
h5: {
fontWeight: 'bold',
fontSize: '0.83em',
},
h6: {
fontWeight: 'bold',
fontSize: '0.67em',
},
p: {
marginTop: '1em',
marginBottom: '1em',
},
pre: {
fontFamily: 'monospace',
whiteSpace: 'pre',
marginTop: '1em',
marginBottom: '1em',
},
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DefaultImplicitFormatMap, DefaultStyleMap } from 'roosterjs-content-model-types';
import { DefaultStyleMap } from 'roosterjs-content-model-types';

const blockElement: Partial<CSSStyleDeclaration> = {
display: 'block',
Expand All @@ -7,7 +7,7 @@ const blockElement: Partial<CSSStyleDeclaration> = {
/**
* @internal
*/
export const defaultStyleMap: DefaultStyleMap = {
export const defaultHTMLStyleMap: DefaultStyleMap = {
address: blockElement,
article: blockElement,
aside: blockElement,
Expand Down Expand Up @@ -123,69 +123,3 @@ export const defaultStyleMap: DefaultStyleMap = {
},
ul: blockElement,
};

/**
* @internal
*/
export const enum PseudoTagNames {
childOfPre = 'pre *', // This value is not a CSS selector, it just to tell this will impact elements under PRE tag. Any unique value here can work actually
}

/**
* A map from tag name to its default implicit formats
*/
export const defaultImplicitFormatMap: DefaultImplicitFormatMap = {
a: {
underline: true,
},
blockquote: {
marginTop: '1em',
marginBottom: '1em',
marginLeft: '40px',
marginRight: '40px',
},
code: {
fontFamily: 'monospace',
},
h1: {
fontWeight: 'bold',
fontSize: '2em',
},
h2: {
fontWeight: 'bold',
fontSize: '1.5em',
},
h3: {
fontWeight: 'bold',
fontSize: '1.17em',
},
h4: {
fontWeight: 'bold',
fontSize: '1em', // Set this default value here to overwrite existing font size when change heading level
},
h5: {
fontWeight: 'bold',
fontSize: '0.83em',
},
h6: {
fontWeight: 'bold',
fontSize: '0.67em',
},
p: {
marginTop: '1em',
marginBottom: '1em',
},
pre: {
fontFamily: 'monospace',
whiteSpace: 'pre',
marginTop: '1em',
marginBottom: '1em',
},

// For PRE tag, the following styles will be included from the PRE tag.
// Adding this implicit style here so no need to generate these style for child elements
[PseudoTagNames.childOfPre]: {
fontFamily: 'monospace',
whiteSpace: 'pre',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defaultProcessorMap } from './defaultProcessors';
import { ElementProcessorMap } from 'roosterjs-content-model-types';

/**
* Build a DOM processor map with overrides that can be used as base processor map
* @param processorOverrides DOM processor overrides to default processors.
* Note: Inside an override processor it cannot call original processor using context.defaultElementProcessors.<ProcessorName>
* since here the default processor is also overridden
*/
export function buildBaseProcessorMap(
...processorOverrides: (Partial<ElementProcessorMap> | undefined)[]
): ElementProcessorMap {
return Object.assign({}, defaultProcessorMap, ...processorOverrides);
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,82 @@
import { defaultFormatParsers, getFormatParsers } from '../../formatHandlers/defaultFormatHandlers';
import { defaultProcessorMap } from './defaultProcessors';
import { defaultStyleMap } from '../../formatHandlers/utils/defaultStyles';
import { DomToModelContext, DomToModelOption, EditorContext } from 'roosterjs-content-model-types';
import { getObjectKeys } from 'roosterjs-editor-dom';
import { SelectionRangeEx } from 'roosterjs-editor-types';
import {
defaultFormatHandlerMap,
defaultFormatKeysPerCategory,
} from '../../formatHandlers/defaultFormatHandlers';
import {
ContentModelBlockFormat,
DomToModelContext,
DomToModelDecoratorContext,
DomToModelFormatContext,
DomToModelSelectionContext,
DomToModelSettings,
EditorContext,
ElementProcessorMap,
FormatParser,
FormatParsers,
FormatParsersPerCategory,
} from 'roosterjs-content-model-types';

/**
* Create context object form DOM to Content Model conversion
* Create context object for DOM to Content Model conversion
* @param processorOverride Overrides default element processors
* @param formatParserOverride Overrides default format handlers
* @param additionalFormatParsers: Provide additional format parsers for each format type
* @param baseProcessorMap Base DOM processor map, if not passed, default processor map will be used
* @param editorContext Context of editor
* @param options Options for this context
* @param selection Selection that already exists in content
*/
export function createDomToModelContext(
editorContext?: EditorContext,
options?: DomToModelOption,
selection?: SelectionRangeEx
processorOverride?: Partial<ElementProcessorMap>,
formatParserOverride?: Partial<FormatParsers>,
additionalFormatParsers?: (Partial<FormatParsersPerCategory> | undefined)[],
baseProcessorMap?: Readonly<ElementProcessorMap>,
selection?: SelectionRangeEx,
editorContext?: EditorContext
): DomToModelContext {
const context: DomToModelContext = {
...editorContext,
return Object.assign(
{},
editorContext,
createDomToModelSelectionContext(selection),
createDomToModelFormatContext(editorContext?.isRootRtl),
createDomToModelDecoratorContext(),
createDomToModelSettings(
processorOverride,
formatParserOverride,
additionalFormatParsers,
baseProcessorMap
)
);
}

function createDomToModelSelectionContext(rangeEx?: SelectionRangeEx): DomToModelSelectionContext {
const result: DomToModelSelectionContext = { isInSelection: false };

if (rangeEx) {
result.rangeEx = rangeEx;
}

return result;
}

blockFormat: {},
function createDomToModelFormatContext(isRootRtl?: boolean): DomToModelFormatContext {
const blockFormat: ContentModelBlockFormat = isRootRtl ? { direction: 'rtl' } : {};

return {
blockFormat,
segmentFormat: {},
isInSelection: false,

listFormat: {
levels: [],
threadItemCounts: [],
},
};
}

function createDomToModelDecoratorContext(): DomToModelDecoratorContext {
return {
link: {
format: {},
dataset: {},
Expand All @@ -37,33 +88,66 @@ export function createDomToModelContext(
format: {},
tagName: '',
},
};
}

elementProcessors: {
...defaultProcessorMap,
...(options?.processorOverride || {}),
},

defaultStyles: {
...defaultStyleMap,
...(options?.defaultStyleOverride || {}),
},

formatParsers: getFormatParsers(
options?.formatParserOverride,
options?.additionalFormatParsers
),
function createDomToModelSettings(
processorOverride?: Partial<ElementProcessorMap>,
formatParserOverride?: Partial<FormatParsers>,
additionalFormatParsers?: (Partial<FormatParsersPerCategory> | undefined)[],
baseProcessorMap?: Readonly<ElementProcessorMap>
): DomToModelSettings {
const defaultElementProcessors = baseProcessorMap ?? defaultProcessorMap;

defaultElementProcessors: defaultProcessorMap,
defaultFormatParsers: defaultFormatParsers,
return {
elementProcessors: processorOverride
? { ...defaultElementProcessors, ...processorOverride }
: defaultElementProcessors,
formatParsers:
formatParserOverride || (additionalFormatParsers?.length ?? 0) > 0
? buildFormatParsers(formatParserOverride, additionalFormatParsers)
: defaultFormatParsersPerCategory,
defaultElementProcessors,
};
}

if (editorContext?.isRootRtl) {
context.blockFormat.direction = 'rtl';
}
const defaultFormatParsers: Readonly<FormatParsers> = getObjectKeys(defaultFormatHandlerMap).reduce(
(result, key) => {
result[key] = defaultFormatHandlerMap[key].parse as FormatParser<any>;
return result;
},
<FormatParsers>{}
);

if (selection) {
context.rangeEx = selection;
}
/**
* @internal Export for test only
* Build format parsers used by DOM to Content Model conversion
* @param override
* @param additionalParsersArray
* @returns
*/
export function buildFormatParsers(
override: Partial<FormatParsers> = {},
additionalParsersArray: (Partial<FormatParsersPerCategory> | undefined)[] = []
): FormatParsersPerCategory {
return getObjectKeys(defaultFormatKeysPerCategory).reduce((result, key) => {
const value = defaultFormatKeysPerCategory[key]
.map(
formatKey =>
(override[formatKey] === undefined
? defaultFormatParsers[formatKey]
: override[formatKey]) as FormatParser<any>
)
.concat(
...additionalParsersArray.map(
parsers => (parsers?.[key] ?? []) as FormatParser<any>[]
)
);

return context;
result[key] = value;

return result;
}, {} as FormatParsersPerCategory);
}

const defaultFormatParsersPerCategory = buildFormatParsers();
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
import { createContentModelDocument } from '../modelApi/creators/createContentModelDocument';
import { createDomToModelContext } from './context/createDomToModelContext';
import { normalizeContentModel } from '../modelApi/common/normalizeContentModel';
import { SelectionRangeEx } from 'roosterjs-editor-types';
import {
ContentModelDocument,
DomToModelOption,
EditorContext,
} from 'roosterjs-content-model-types';
import { ContentModelDocument, DomToModelContext } from 'roosterjs-content-model-types';

/**
* Create Content Model from DOM tree in this editor
* @param root Root element of DOM tree to create Content Model from
* @param option The option to customize the behavior of DOM to Content Model conversion
* @param editorContext Context of content model editor
* @param selection Existing selection range in editor
* @param config DOM Processor and format parser configuration
* @param editorContext Context of editor
* @param selection Selection that already exists in content
* @returns A ContentModelDocument object that contains all the models created from the give root element
*/
export function domToContentModel(
root: HTMLElement | DocumentFragment,
option?: DomToModelOption,
editorContext?: EditorContext,
selection?: SelectionRangeEx
context: DomToModelContext
): ContentModelDocument {
const model = createContentModelDocument(editorContext?.defaultFormat);
const context = createDomToModelContext(editorContext, option, selection);
const model = createContentModelDocument(context.defaultFormat);

context.elementProcessors.child(model, root, context);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { defaultHTMLStyleMap } from '../../config/defaultHTMLStyleMap';
import { DefaultStyleMap, DomToModelContext } from 'roosterjs-content-model-types';

/**
Expand All @@ -13,5 +14,5 @@ export function getDefaultStyle(
): Partial<CSSStyleDeclaration> {
let tag = element.tagName.toLowerCase() as keyof DefaultStyleMap;

return context.defaultStyles[tag] || {};
return defaultHTMLStyleMap[tag] || {};
}
Loading
Loading