Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4459182
fix(markdown): accept angle-bracketed, data/blob and relative URLs fo…
francismengMS Mar 23, 2026
6fba0f4
fix test
francismengMS Mar 23, 2026
5c5f6db
Merge branch 'master' into fix/markdown-image-url-parse
FrancisMengx Mar 23, 2026
6ffcc57
fix test
francismengMS Mar 26, 2026
a6d6fb3
Merge branch 'fix/markdown-image-url-parse' of https://github.com/mic…
francismengMS Mar 26, 2026
edac6a3
update
francismengMS Mar 26, 2026
f7cc9dc
Merge branch 'master' into fix/markdown-image-url-parse
FrancisMengx Mar 26, 2026
a7f338d
Bump picomatch from 2.2.1 to 2.3.2 (#3314)
dependabot[bot] Mar 30, 2026
e6198df
fix(markdown): validate URL protocol and add tests for splitParagraph…
francismengMS Mar 30, 2026
9e5380b
Merge branch 'master' into fix/markdown-image-url-parse
FrancisMengx Mar 30, 2026
c65d5a8
Bump handlebars from 4.7.7 to 4.7.9 (#3315)
dependabot[bot] Mar 30, 2026
0aa95f3
Merge branch 'master' into fix/markdown-image-url-parse
FrancisMengx Mar 30, 2026
0ba436b
Merge pull request #3312 from microsoft/fix/markdown-image-url-parse
FrancisMengx Mar 30, 2026
e429c23
Prevent drop malicious content on the editor (#3319)
juliaroldi Apr 9, 2026
576e6cb
Bump lodash from 4.17.23 to 4.18.1 (#3321)
dependabot[bot] Apr 10, 2026
971eb40
Bump follow-redirects from 1.15.6 to 1.16.0 (#3322)
dependabot[bot] Apr 21, 2026
2f85ab6
Bump dompurify from 2.5.4 to 3.4.0 (#3323)
dependabot[bot] Apr 21, 2026
0b42312
Revert "Strip invisible Unicode from content model at editor initiali…
JiuqingSong Apr 22, 2026
f37616a
Fix 422986 (#3325)
JiuqingSong Apr 23, 2026
7a46ec4
Graduate some experimental features (#3327)
JiuqingSong Apr 23, 2026
aa934e7
Merge branch 'master' into u/nguyenvi/versionbump042426
vinguyen12 Apr 24, 2026
b9e2819
fix version file
vinguyen12 Apr 24, 2026
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
Expand Up @@ -70,12 +70,7 @@ const initialState: OptionState = {
},
customReplacements: emojiReplacements,
disableSideResize: false,
experimentalFeatures: new Set<ExperimentalFeature>([
'HandleEnterKey',
'CloneIndependentRoot',
'CacheList',
'TransformTableBorderColors',
]),
experimentalFeatures: new Set<ExperimentalFeature>(['TransformTableBorderColors']),
};

export class EditorOptionsPlugin extends SidePanePluginImpl<OptionsPane, OptionPaneProps> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ export class ExperimentalFeatures extends React.Component<DefaultFormatProps, {}
render() {
return (
<>
{this.renderFeature('HandleEnterKey')}
{this.renderFeature('KeepSelectionMarkerWhenEnteringTextNode')}
{this.renderFeature('CloneIndependentRoot')}
{this.renderFeature('CacheList')}
{this.renderFeature('TransformTableBorderColors')}
</>
);
Expand Down
8 changes: 0 additions & 8 deletions demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ abstract class PluginsBase<PluginKey extends keyof BuildInPluginList> extends Re
export class Plugins extends PluginsBase<keyof BuildInPluginList> {
private allowExcelNoBorderTable = React.createRef<HTMLInputElement>();
private handleTabKey = React.createRef<HTMLInputElement>();
private handleEnterKey = React.createRef<HTMLInputElement>();
private listMenu = React.createRef<HTMLInputElement>();
private tableMenu = React.createRef<HTMLInputElement>();
private imageMenu = React.createRef<HTMLInputElement>();
Expand Down Expand Up @@ -240,13 +239,6 @@ export class Plugins extends PluginsBase<keyof BuildInPluginList> {
(state, value) =>
(state.editPluginOptions.handleTabKey.indentParagraph = value)
)}
{this.renderCheckBox(
'Handle Enter Key',
this.handleEnterKey,
this.props.state.editPluginOptions.shouldHandleEnterKey as boolean,
(state, value) =>
(state.editPluginOptions.shouldHandleEnterKey = value)
)}
</>
)}
{this.renderPluginItem(
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"coverage-istanbul-loader": "3.0.5",
"css-loader": "3.5.3",
"detect-port": "^1.3.0",
"dompurify": "2.5.4",
"dompurify": "3.4.0",
"eslint": "^8.50.0",
"eslint-plugin-etc": "^2.0.3",
"eslint-plugin-react": "^7.33.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
/**
* Export HTML content. If there are entities, this will cause EntityOperation event with option = 'replaceTemporaryContent' to get a dehydrated entity
* @param editor The editor to get content from
* @param mode Specify HTML to get HTML. This is the default option
* @param mode Specify HTML to get HTML.
* @param options @optional Options for Model to DOM conversion
*/
export function exportContent(editor: IEditor, mode?: 'HTML', options?: ModelToDomOption): string;
Expand All @@ -24,9 +24,9 @@ export function exportContent(editor: IEditor, mode?: 'HTML', options?: ModelToD
* Export HTML content. If there are entities, this will cause EntityOperation event with option = 'replaceTemporaryContent' to get a dehydrated entity.
* This is a fast version, it retrieve HTML content directly from editor without going through content model conversion.
* @param editor The editor to get content from
* @param mode Specify HTMLFast to get HTML result.
* @param mode Specify HTMLFast to get HTML result. This is the default option
*/
export function exportContent(editor: IEditor, mode: 'HTMLFast'): string;
export function exportContent(editor: IEditor, mode?: 'HTMLFast'): string;

/**
* Export plain text content
Expand All @@ -52,7 +52,7 @@ export function exportContent(editor: IEditor, mode: 'PlainTextFast'): string;
// Once we are confident that 'HTMLFast' is stable, we can fully switch 'HTML' to use the 'HTMLFast' approach
export function exportContent(
editor: IEditor,
mode: ExportContentMode | 'HTMLFast' = 'HTML',
mode: ExportContentMode | 'HTMLFast' = 'HTMLFast',
optionsOrCallbacks?: ModelToDomOption | ModelToTextCallbacks
): string {
let model: ContentModelDocument;
Expand All @@ -70,6 +70,7 @@ export function exportContent(
);

case 'HTMLFast':
default:
const clonedRoot = editor.getDOMHelper().getClonedRoot();

if (editor.isDarkMode()) {
Expand All @@ -89,7 +90,6 @@ export function exportContent(
return getHTMLFromDOM(editor, clonedRoot);

case 'HTML':
default:
model = editor.getContentModelCopy('clean');

const doc = editor.getDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export const createEditorContext: CreateEditorContext = (core, saveIndex) => {
darkColorHandler: darkColorHandler,
addDelimiterForEntity: true,
allowCacheElement: true,
allowCacheListItem: !!core.experimentalFeatures?.includes('CacheList'),
domIndexer: saveIndex ? cache.domIndexer : undefined,
zoomScale: domHelper.calculateZoomScale(),
experimentalFeatures: core.experimentalFeatures ?? [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ import type {
* @internal
*/
export interface DOMHelperImplOption {
/**
* @deprecated This is always treated as true now
*/
cloneIndependentRoot?: boolean;
}

class DOMHelperImpl implements DOMHelper {
constructor(private contentDiv: HTMLElement, private options: DOMHelperImplOption) {}
constructor(private contentDiv: HTMLElement, options?: DOMHelperImplOption) {}

queryElements(selector: string): HTMLElement[] {
return toArray(this.contentDiv.querySelectorAll(selector)) as HTMLElement[];
Expand Down Expand Up @@ -123,14 +126,10 @@ class DOMHelperImpl implements DOMHelper {
* Get a deep cloned root element
*/
getClonedRoot(): HTMLElement {
if (this.options.cloneIndependentRoot) {
const doc = this.contentDiv.ownerDocument.implementation.createHTMLDocument();
const clone = doc.importNode(this.contentDiv, true /*deep*/);
const doc = this.contentDiv.ownerDocument.implementation.createHTMLDocument();
const clone = doc.importNode(this.contentDiv, true /*deep*/);

return clone;
} else {
return this.contentDiv.cloneNode(true /*deep*/) as HTMLElement;
}
return clone;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ export function createEditorCore(contentDiv: HTMLDivElement, options: EditorOpti
? options.trustedHTMLHandler
: createTrustedHTMLHandler(domCreator),
domCreator: domCreator,
domHelper: createDOMHelper(contentDiv, {
cloneIndependentRoot: options.experimentalFeatures?.includes('CloneIndependentRoot'),
}),
domHelper: createDOMHelper(contentDiv),
...getPluginState(corePlugins),
disposeErrorHandler: options.disposeErrorHandler,
onFixUpModel: options.onFixUpModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ describe('createEditorContext', () => {
experimentalFeatures: [],
paragraphMap: mockedParagraphMap,
editorViewWidth: 800,
allowCacheListItem: false,
});
});

Expand Down Expand Up @@ -104,7 +103,6 @@ describe('createEditorContext', () => {
experimentalFeatures: [],
paragraphMap: mockedParagraphMap,
editorViewWidth: 800,
allowCacheListItem: false,
});
});

Expand Down Expand Up @@ -154,7 +152,6 @@ describe('createEditorContext', () => {
experimentalFeatures: [],
paragraphMap: mockedParagraphMap,
editorViewWidth: 800,
allowCacheListItem: false,
});
});

Expand Down Expand Up @@ -207,7 +204,6 @@ describe('createEditorContext', () => {
experimentalFeatures: [],
paragraphMap: mockedParagraphMap,
editorViewWidth: 800,
allowCacheListItem: false,
});
});
});
Expand Down Expand Up @@ -266,7 +262,6 @@ describe('createEditorContext - checkZoomScale', () => {
experimentalFeatures: [],
paragraphMap: mockedParagraphMap,
editorViewWidth: 800,
allowCacheListItem: false,
});
});
});
Expand Down Expand Up @@ -325,7 +320,6 @@ describe('createEditorContext - checkRootDir', () => {
experimentalFeatures: [],
paragraphMap: mockedParagraphMap,
editorViewWidth: 800,
allowCacheListItem: false,
});
});

Expand All @@ -347,7 +341,6 @@ describe('createEditorContext - checkRootDir', () => {
experimentalFeatures: [],
paragraphMap: mockedParagraphMap,
editorViewWidth: 800,
allowCacheListItem: false,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -354,20 +354,6 @@ describe('DOMHelperImpl', () => {

describe('getClonedRoot', () => {
it('getClonedRoot', () => {
const mockedClone = 'CLONE' as any;
const cloneSpy = jasmine.createSpy('cloneSpy').and.returnValue(mockedClone);
const mockedDiv: HTMLElement = {
cloneNode: cloneSpy,
} as any;
const domHelper = createDOMHelper(mockedDiv);

const result = domHelper.getClonedRoot();

expect(result).toBe(mockedClone);
expect(cloneSpy).toHaveBeenCalledWith(true);
});

it('getClonedRoot, with CloneIndependentRoot on', () => {
const mockedClone = 'CLONE' as any;
const cloneSpy = jasmine.createSpy('cloneSpy').and.returnValue(mockedClone);
const importNodeSpy = jasmine.createSpy('importNodeSpy').and.returnValue(mockedClone);
Expand All @@ -381,9 +367,7 @@ describe('DOMHelperImpl', () => {
},
},
} as any;
const domHelper = createDOMHelper(mockedDiv, {
cloneIndependentRoot: true,
});
const domHelper = createDOMHelper(mockedDiv);

const result = domHelper.getClonedRoot();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createListItem } from '../../modelApi/creators/createListItem';
import { createListLevel } from '../../modelApi/creators/createListLevel';
import { parseFormat } from '../utils/parseFormat';
import { stackFormat } from '../utils/stackFormat';
import type { ElementProcessor } from 'roosterjs-content-model-types';
Expand All @@ -8,8 +9,21 @@ import type { ElementProcessor } from 'roosterjs-content-model-types';
*/
export const listItemProcessor: ElementProcessor<HTMLLIElement> = (group, element, context) => {
const { listFormat } = context;
const originalListParent = listFormat.listParent;
let shouldPopListLevel = false;

try {
listFormat.listParent = listFormat.listParent ?? group;

const listParent = listFormat.listParent;

if (listFormat.levels.length == 0) {
listFormat.levels.push(
createListLevel(listFormat.potentialListType || 'UL', context.blockFormat)
);
shouldPopListLevel = true;
}

if (listFormat.listParent && listFormat.levels.length > 0) {
stackFormat(
context,
{
Expand All @@ -31,7 +45,7 @@ export const listItemProcessor: ElementProcessor<HTMLLIElement> = (group, elemen
context
);

listFormat.listParent!.blocks.push(listItem);
listParent.blocks.push(listItem);

parseFormat(
element,
Expand All @@ -54,14 +68,11 @@ export const listItemProcessor: ElementProcessor<HTMLLIElement> = (group, elemen
}
}
);
} else {
const currentBlocks = listFormat.listParent?.blocks;
const lastItem = currentBlocks?.[currentBlocks?.length - 1];
} finally {
if (shouldPopListLevel) {
listFormat.levels.pop();
}

context.elementProcessors['*'](
lastItem?.blockType == 'BlockGroup' ? lastItem : group,
element,
context
);
listFormat.listParent = originalListParent;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ export const listProcessor: ElementProcessor<HTMLOListElement | HTMLUListElement
paragraph: 'shallowCloneForGroup',
},
() => {
const level: ContentModelListLevel = createListLevel(
element.tagName as 'OL' | 'UL',
context.blockFormat
);
const tagName = element.tagName as 'OL' | 'UL';
const level: ContentModelListLevel = createListLevel(tagName, context.blockFormat);
const { listFormat } = context;

parseFormat(element, context.formatParsers.dataset, level.dataset, context);
Expand All @@ -31,6 +29,7 @@ export const listProcessor: ElementProcessor<HTMLOListElement | HTMLUListElement

const originalListParent = listFormat.listParent;

listFormat.potentialListType = tagName;
listFormat.listParent = listFormat.listParent || group;
listFormat.levels.push(level);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const handleBlockGroupChildren: ContentModelHandler<ContentModelBlockGrou
};

function cleanUpNodeStack(nodeStack: ModelToDomListStackItem[], context: ModelToDomContext) {
if (context.allowCacheListItem && nodeStack.length > 0) {
if (nodeStack.length > 0) {
// Clear list stack, only run to nodeStack[1] because nodeStack[0] is the parent node
for (let i = nodeStack.length - 1; i > 0; i--) {
const node = nodeStack.pop()?.refNode ?? null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,18 @@ export const handleList: ContentModelBlockHandler<ContentModelListItem> = (
applyMetadata(itemLevel, context.metadataAppliers.listLevel, itemLevel.format, context);
}

if (
context.allowCacheListItem &&
parentLevel.refNode &&
itemLevel.cachedElement == parentLevel.refNode
) {
if (parentLevel.refNode && itemLevel.cachedElement == parentLevel.refNode) {
// Move refNode to next node since we are reusing this cached element
parentLevel.refNode = parentLevel.refNode.nextSibling;
}
}

// Cut off remained list levels that we can't reuse
if (context.allowCacheListItem) {
// Clean up all rest nodes in the reused list levels
for (let i = layer + 1; i < nodeStack.length; i++) {
const stackLevel = nodeStack[i];
// Clean up all rest nodes in the reused list levels
for (let i = layer + 1; i < nodeStack.length; i++) {
const stackLevel = nodeStack[i];

cleanUpRestNodes(stackLevel.refNode, context.rewriteFromModel);
}
cleanUpRestNodes(stackLevel.refNode, context.rewriteFromModel);
}

nodeStack.splice(layer + 1);
Expand All @@ -83,7 +77,7 @@ export const handleList: ContentModelBlockHandler<ContentModelListItem> = (

context.listFormat.currentLevel = layer;

if (context.allowCacheListItem && level.cachedElement) {
if (level.cachedElement) {
newList = level.cachedElement;

nodeStack[layer].refNode = reuseCachedElement(
Expand Down Expand Up @@ -112,9 +106,7 @@ export const handleList: ContentModelBlockHandler<ContentModelListItem> = (
dataset: { ...level.dataset },
});

if (context.allowCacheListItem) {
level.cachedElement = newList;
}
level.cachedElement = newList;
}

applyFormat(newList, context.formatAppliers.listLevelThread, level.format, context);
Expand Down
Loading
Loading