diff --git a/.claude/skills/version-bump/SKILL.md b/.claude/skills/version-bump/SKILL.md new file mode 100644 index 000000000000..c3fd4f55befb --- /dev/null +++ b/.claude/skills/version-bump/SKILL.md @@ -0,0 +1,226 @@ +--- +name: version-bump +description: Performs a version bump for roosterjs. Merges changes from master into release, determines the correct SemVer version bump based on public interface changes, and creates a draft PR. Use when asked to do a version bump, release prep, or bump versions. +--- + +# Version Bump Skill + +This skill performs a version bump for roosterjs. It merges changes from `master` into `release`, determines the correct SemVer version bump based on public interface changes, and creates a draft PR. + +## Steps + +### Step 1: Check for uncommitted changes + +Run `git status --porcelain`. If there is any output (uncommitted changes exist), **stop immediately** and ask the user to deal with their uncommitted changes first before proceeding. + +### Step 2: Switch to master and pull latest + +```bash +git checkout master +git pull origin master +``` + +If either command fails, stop and report the error. + +### Step 3: Switch to release and pull latest + +```bash +git checkout release +git pull origin release +``` + +If either command fails, stop and report the error. + +### Step 4: Find the last version bump commit on release + +Search the git log on the `release` branch for the most recent version bump commit. Version bump commits typically have "Version bump" or "version bump" in their commit message, or modify only `versions.json`. + +```bash +git log release --oneline --grep="ersion bump" -1 +``` + +If not found via message, look for the last commit that modified `versions.json`: + +```bash +git log release --oneline -1 -- versions.json +``` + +Record the commit hash and date of this commit. + +### Step 5: Find all PRs merged into master after the last version bump + +Using the date/hash from Step 4, find all merge commits (PRs) merged into `master` after that point: + +```bash +git log master --oneline --merges --after="" +``` + +Alternatively, find commits on master that are not on release: + +```bash +git log release..master --oneline --merges +``` + +If **no PRs are found**, stop and tell the user: "Version bump is not required since there is no PR merged since the last version bump." + +### Step 6: Create PR descriptions + +For each PR found in Step 5, create a one-line description with the PR link. Format: + +``` +- # (https://github.com/microsoft/roosterjs/pull/) +``` + +Use `gh pr view --json title,number,url` to get details if needed. Save these descriptions for use in Step 15. + +### Step 7: Create a new branch based on release + +Create a new branch from `release`. Use the naming convention `u//bump-` where N is incremented, or as specified by the user: + +```bash +git checkout -b release +``` + +### Step 8: Merge master using "accept theirs" strategy + +Merge master into the new branch, preferring master's changes for any conflicts: + +```bash +git merge master -X theirs +``` + +### Step 9: Verify no unresolved conflicts + +Check for any remaining conflict markers: + +```bash +git diff --check +grep -r "<<<<<<" --include="*.ts" --include="*.js" --include="*.json" . +``` + +If conflicts remain, stop and report them to the user. + +### Step 10: Compare current branch with master + +Run a diff between the current branch and master: + +```bash +git diff master -- . ':!versions.json' +``` + +Ignore differences that are **only** whitespace/formatting (newlines, indentation). You can verify with: + +```bash +git diff master --stat -- . ':!versions.json' +``` + +If there are substantive code differences (not just formatting), show the differences to the user and ask: "There are code differences between this branch and master (other than versions.json). Do you want to continue?" + +If the user says no, stop the flow. + +### Step 11: Update versions.json with SemVer bump + +The `versions.json` file has 4 version groups: + +| Group | Packages | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `main` | roosterjs, roosterjs-content-model-types, roosterjs-content-model-dom, roosterjs-content-model-core, roosterjs-content-model-api, roosterjs-content-model-plugins, roosterjs-color-utils, roosterjs-content-model-markdown | +| `legacyAdapter` | roosterjs-editor-adapter | +| `react` | roosterjs-react | +| `overrides` | Per-package overrides (usually empty) | + +The grouping is defined in `/tools/buildTools/common.js` in the `buildConfig` object. + +**SemVer rules:** + +- **Patch bump** (0.0.x → 0.0.x+1): Only bug fixes, no API changes +- **Minor bump** (0.x.0 → 0.x+1.0): New features/APIs added, but backward-compatible +- **Major bump** (x.0.0 → x+1.0.0): Breaking changes to public interfaces + +To determine the bump level: + +1. Compare the exported types/interfaces between `release` and the merged code +2. Look at the `lib/index.ts` barrel files in each package for added/removed/changed exports +3. Check if any existing public interface signatures changed (breaking = major) +4. Check if new exports were added (non-breaking addition = minor) +5. If only implementation changes with no public API changes = patch + +**If a major version bump appears needed**, show the interface differences to the user and ask: "A major version bump seems needed due to these interface changes. Do you want to bump the major version, or just bump minor instead?" + +Update `versions.json` with the new version numbers for each affected group. + +### Step 12: Build and test + +Run the full build and test suite: + +```bash +yarn build +yarn test:fast +``` + +If **any errors** occur, show the full error output and **stop the flow**. Do not proceed with a broken build. + +### Step 13: Commit the change + +```bash +git add versions.json +git commit -m "Version bump to " +``` + +Include all changed version numbers in the commit message. + +### Step 14: Push the branch + +```bash +git push origin +``` + +### Step 15: Create a draft PR + +Create a draft PR targeting the `release` branch using the `gh` CLI: + +**Title:** `Version bump : ` (list all groups that changed) + +**Description:** Include: + +1. A table showing old and new versions for each group: + +```markdown +| Group | Old Version | New Version | +| ------------- | ----------- | ----------- | +| main | x.y.z | x.y.z+1 | +| react | x.y.z | x.y.z+1 | +| legacyAdapter | x.y.z | x.y.z+1 | +``` + +2. The PR descriptions from Step 6: + +```markdown +## Changes included + +- #123 Add feature X (https://github.com/microsoft/roosterjs/pull/123) +- #124 Fix bug Y (https://github.com/microsoft/roosterjs/pull/124) +``` + +Command: + +```bash +gh pr create --draft --base release --title "" --body "<description>" +``` + +### Step 16: Show the PR link + +Display the PR URL to the user. Example output: + +``` +✅ Version bump PR created successfully! +PR: https://github.com/microsoft/roosterjs/pull/<number> +``` + +## Error Handling + +- If any git operation fails, show the error and stop +- If build/test fails, show errors and stop +- If there are unexpected code differences, ask the user before continuing +- If major version bump is detected, confirm with user before applying +- Always leave the repo in a clean state if stopping early diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 7b80d8b62509..76a6527334ac 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -39,3 +39,4 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: gh-pages FOLDER: dist/deploy + CLEAN_EXCLUDE: '["pr-preview/**"]' diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 000000000000..89bca09d2259 --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,48 @@ +name: Deploy PR Preview +on: + pull_request: + types: + - opened + - reopened + - synchronize + - closed + +concurrency: preview-${{ github.ref }} + +jobs: + deploy-preview: + # Skip PRs from forks: GITHUB_TOKEN is read-only for fork PRs, + # so it cannot push to gh-pages. Fork support requires a separate + # workflow_run-based design. + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set Node Version + if: github.event.action != 'closed' + uses: actions/setup-node@v4 + with: + node-version: 'v18.16.0' + + - name: Install dependencies + if: github.event.action != 'closed' + run: yarn + + - name: Build + if: github.event.action != 'closed' + run: yarn build + + - name: Deploy preview + # Pinned to commit SHA (v1.8.1) for supply-chain safety, since + # this third-party action runs with write access to gh-pages and PRs. + uses: rossjrw/pr-preview-action@ffa7509e91a3ec8dfc2e5536c4d5c1acdf7a6de9 # v1.8.1 + with: + source-dir: ./dist/deploy + preview-branch: gh-pages + umbrella-dir: pr-preview + action: auto diff --git a/demo/scripts/controlsV2/demoButtons/pasteButton.ts b/demo/scripts/controlsV2/demoButtons/pasteButton.ts index 071f5bf824d3..b7a23d091700 100644 --- a/demo/scripts/controlsV2/demoButtons/pasteButton.ts +++ b/demo/scripts/controlsV2/demoButtons/pasteButton.ts @@ -2,6 +2,10 @@ import { extractClipboardItems } from 'roosterjs-content-model-dom'; import { paste } from 'roosterjs-content-model-core'; import type { RibbonButton } from 'roosterjs-react'; +interface ClipboardWithUnsanitized { + read(options?: { unsanitized?: string[] }): Promise<ClipboardItems>; +} + /** * @internal * "Paste" button on the format ribbon @@ -12,10 +16,10 @@ export const pasteButton: RibbonButton<'buttonNamePaste'> = { iconName: 'Paste', onClick: async editor => { const doc = editor.getDocument(); - const clipboard = doc.defaultView.navigator.clipboard; + const clipboard = doc.defaultView.navigator.clipboard as ClipboardWithUnsanitized; if (clipboard && clipboard.read) { try { - const clipboardItems = await clipboard.read(); + const clipboardItems = await clipboard.read({ unsanitized: ['text/html'] }); const dataTransferItems = await Promise.all( createDataTransferItems(clipboardItems) ); diff --git a/demo/scripts/controlsV2/mainPane/MainPane.tsx b/demo/scripts/controlsV2/mainPane/MainPane.tsx index 6dc8a389581d..cbaf7ebd967a 100644 --- a/demo/scripts/controlsV2/mainPane/MainPane.tsx +++ b/demo/scripts/controlsV2/mainPane/MainPane.tsx @@ -353,11 +353,40 @@ export class MainPane extends React.Component<{}, MainPaneState> { ); } + private shadowDomEditorDiv: HTMLDivElement | undefined; private resetEditor() { + const useShadowDom = this.editorOptionPlugin + .getBuildInPluginState() + .experimentalFeatures.has('ShadowDom'); + this.setState({ - editorCreator: (div: HTMLDivElement, options: EditorOptions) => { - return new Editor(div, options); - }, + editorCreator: useShadowDom + ? (div: HTMLDivElement, options: EditorOptions) => { + while (div.firstChild) { + div.removeChild(div.firstChild); + } + const newDivHost = document.createElement('div'); + div.appendChild(newDivHost); + const shadowRoot = newDivHost.attachShadow({ mode: 'open' }); + const innerDiv = document.createElement('div'); + innerDiv.style.width = '100%'; + innerDiv.style.height = '100%'; + innerDiv.style.outline = 'none'; + shadowRoot.appendChild(innerDiv); + this.shadowDomEditorDiv = newDivHost; + const editor = new Editor(innerDiv, options); + + div.setAttribute('style', newDivHost.getAttribute('style') || ''); + newDivHost.style.width = '100%'; + newDivHost.style.height = '100%'; + + return editor; + } + : (div: HTMLDivElement, options: EditorOptions) => { + this.shadowDomEditorDiv?.remove(); + this.shadowDomEditorDiv = undefined; + return new Editor(div, options); + }, }); } diff --git a/demo/scripts/controlsV2/sidePane/apiPlayground/paste/PastePane.tsx b/demo/scripts/controlsV2/sidePane/apiPlayground/paste/PastePane.tsx index 45d0703cdb2e..645a5bc1d16f 100644 --- a/demo/scripts/controlsV2/sidePane/apiPlayground/paste/PastePane.tsx +++ b/demo/scripts/controlsV2/sidePane/apiPlayground/paste/PastePane.tsx @@ -15,6 +15,10 @@ interface PastePaneState { let lastClipboardData: ClipboardData | undefined = undefined; +interface ClipboardWithUnsanitized { + read(options?: { unsanitized?: string[] }): Promise<ClipboardItems>; +} + export default class PastePane extends React.Component<ApiPaneProps, PastePaneState> implements ApiPlaygroundComponent { private clipboardDataRef = React.createRef<HTMLTextAreaElement>(); @@ -72,10 +76,10 @@ export default class PastePane extends React.Component<ApiPaneProps, PastePaneSt private onExtractClipboardProgrammatically = async () => { const doc = this.clipboardDataRef.current.ownerDocument; - const clipboard = doc.defaultView.navigator.clipboard; + const clipboard = doc.defaultView.navigator.clipboard as ClipboardWithUnsanitized; if (clipboard && clipboard.read) { try { - const clipboardItems = await clipboard.read(); + const clipboardItems = await clipboard.read({ unsanitized: ['text/html'] }); const dataTransferItems = await Promise.all( createDataTransferItems(clipboardItems) ); diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx b/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx index 7de49ba7833f..da5332075a12 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx +++ b/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx @@ -13,6 +13,7 @@ export class ExperimentalFeatures extends React.Component<DefaultFormatProps, {} <> {this.renderFeature('KeepSelectionMarkerWhenEnteringTextNode')} {this.renderFeature('TransformTableBorderColors')} + {this.renderFeature('ShadowDom')} </> ); } diff --git a/packages/roosterjs-content-model-core/lib/command/cutCopy/getContentForCopy.ts b/packages/roosterjs-content-model-core/lib/command/cutCopy/getContentForCopy.ts index f881dffbf9ea..c2a7adc8b10f 100644 --- a/packages/roosterjs-content-model-core/lib/command/cutCopy/getContentForCopy.ts +++ b/packages/roosterjs-content-model-core/lib/command/cutCopy/getContentForCopy.ts @@ -4,7 +4,9 @@ import { onCreateCopyEntityNode } from '../../override/pasteCopyBlockEntityParse import { contentModelToDom, contentModelToText, + createDomToModelContext, createModelToDomContext, + domToContentModel, trimModelForSelection, isElementOfType, isNodeOfType, @@ -69,9 +71,13 @@ export function getContentForCopy( isCut, }); + // Build the text content from the (possibly modified) cloned root DOM tree so that any + // changes made by beforeCutCopy event handlers are reflected in the plain text result as well + const textModel = domToContentModel(clonedRoot, createDomToModelContext()); + return { htmlContent: clonedRoot, - textContent: contentModelToText(pasteModel), + textContent: contentModelToText(textModel), }; } } diff --git a/packages/roosterjs-content-model-core/lib/coreApi/announce/announce.ts b/packages/roosterjs-content-model-core/lib/coreApi/announce/announce.ts index 59490603bbab..a8767f1b9b83 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/announce/announce.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/announce/announce.ts @@ -17,6 +17,7 @@ export const announce: Announce = (core, announceData) => { if (!core.lifecycle.announceContainer) { core.lifecycle.announceContainer = createAriaLiveElement(core.physicalRoot.ownerDocument); + core.domHelper.appendToRoot(core.lifecycle.announceContainer); } if (textToAnnounce && core.lifecycle.announceContainer) { diff --git a/packages/roosterjs-content-model-core/lib/coreApi/getDOMSelection/getDOMSelection.ts b/packages/roosterjs-content-model-core/lib/coreApi/getDOMSelection/getDOMSelection.ts index 63c77110276c..a265df838efc 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/getDOMSelection/getDOMSelection.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/getDOMSelection/getDOMSelection.ts @@ -16,16 +16,20 @@ export const getDOMSelection: GetDOMSelection = core => { }; function getNewSelection(core: EditorCore): DOMSelection | null { + const range = core.domHelper.getSelectionRange(); + + if (!range || !core.logicalRoot.contains(range.commonAncestorContainer)) { + return null; + } + const selection = core.logicalRoot.ownerDocument.defaultView?.getSelection(); - const range = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null; + const isReverted = selection + ? selection.focusNode != range.endContainer || selection.focusOffset != range.endOffset + : false; - return selection && range && core.logicalRoot.contains(range.commonAncestorContainer) - ? { - type: 'range', - range, - isReverted: - selection.focusNode != range.endContainer || - selection.focusOffset != range.endOffset, - } - : null; + return { + type: 'range', + range, + isReverted, + }; } diff --git a/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/addRangeToSelection.ts b/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/addRangeToSelection.ts deleted file mode 100644 index b0dc3607d3f6..000000000000 --- a/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/addRangeToSelection.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { areSameRanges } from '../../corePlugin/cache/areSameSelections'; - -/** - * @internal - */ -export function addRangeToSelection(doc: Document, range: Range, isReverted: boolean = false) { - const selection = doc.defaultView?.getSelection(); - - if (selection) { - const currentRange = selection.rangeCount > 0 && selection.getRangeAt(0); - if (currentRange && areSameRanges(currentRange, range)) { - return; - } - selection.removeAllRanges(); - - if (!isReverted) { - selection.addRange(range); - } else { - selection.setBaseAndExtent( - range.endContainer, - range.endOffset, - range.startContainer, - range.startOffset - ); - } - } -} diff --git a/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts b/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts index 3676ed67ef24..4fa3aea02bcb 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts @@ -1,4 +1,3 @@ -import { addRangeToSelection } from './addRangeToSelection'; import { areSameSelections } from '../../corePlugin/cache/areSameSelections'; import { ensureUniqueId } from '../setEditorStyle/ensureUniqueId'; import { findLastedCoInMergedCell } from './findLastedCoInMergedCell'; @@ -6,7 +5,11 @@ import { findTableCellElement } from './findTableCellElement'; import { getSafeIdSelector, parseTableCells } from 'roosterjs-content-model-dom'; import { setTableCellsStyle } from './setTableCellsStyle'; import { toggleCaret } from './toggleCaret'; -import type { SelectionChangedEvent, SetDOMSelection } from 'roosterjs-content-model-types'; +import type { + EditorCore, + SelectionChangedEvent, + SetDOMSelection, +} from 'roosterjs-content-model-types'; const DOM_SELECTION_CSS_KEY = '_DOMSelection'; const HIDE_SELECTION_CSS_KEY = '_DOMSelectionHideSelection'; @@ -29,7 +32,6 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC // Set skipReselectOnFocus to skip this behavior const skipReselectOnFocus = core.selection.skipReselectOnFocus; - const doc = core.physicalRoot.ownerDocument; const isDarkMode = core.lifecycle.isDarkMode; core.selection.skipReselectOnFocus = true; core.api.setEditorStyle(core, DOM_SELECTION_CSS_KEY, null /*cssRule*/); @@ -63,7 +65,7 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC [SELECTION_SELECTOR] ); - setRangeSelection(doc, image, false /* collapse */); + setRangeSelection(core, image, false /* collapse */); break; case 'table': const { table, firstColumn, firstRow, lastColumn, lastRow } = selection; @@ -116,7 +118,7 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC if (nodeToSelect) { setRangeSelection( - doc, + core, (nodeToSelect as HTMLElement) || undefined, true /* collapse */ ); @@ -124,7 +126,7 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC break; case 'range': - addRangeToSelection(doc, selection.range, selection.isReverted); + core.domHelper.setSelectionRange(selection.range, selection.isReverted); core.selection.selection = core.domHelper.hasFocus() ? null : selection; break; @@ -147,8 +149,9 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC } }; -function setRangeSelection(doc: Document, element: HTMLElement | undefined, collapse: boolean) { - if (element && doc.contains(element)) { +function setRangeSelection(core: EditorCore, element: HTMLElement | undefined, collapse: boolean) { + if (element && core.domHelper.isNodeInEditor(element)) { + const doc = core.physicalRoot.ownerDocument; const range = doc.createRange(); let isReverted: boolean | undefined = undefined; @@ -165,6 +168,6 @@ function setRangeSelection(doc: Document, element: HTMLElement | undefined, coll } } - addRangeToSelection(doc, range, isReverted); + core.domHelper.setSelectionRange(range, isReverted); } } diff --git a/packages/roosterjs-content-model-core/lib/coreApi/setEditorStyle/ensureUniqueId.ts b/packages/roosterjs-content-model-core/lib/coreApi/setEditorStyle/ensureUniqueId.ts index 00bb8aaeabb0..236f5718d986 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/setEditorStyle/ensureUniqueId.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/setEditorStyle/ensureUniqueId.ts @@ -6,10 +6,10 @@ import { getSafeIdSelector } from 'roosterjs-content-model-dom'; export function ensureUniqueId(element: HTMLElement, idPrefix: string): string { idPrefix = element.id || idPrefix; - const doc = element.ownerDocument; + const root = element.getRootNode() as Document | ShadowRoot; let i = 0; - while (!element.id || doc.querySelectorAll(getSafeIdSelector(element.id)).length > 1) { + while (!element.id || root.querySelectorAll(getSafeIdSelector(element.id)).length > 1) { element.id = idPrefix + '_' + i++; } diff --git a/packages/roosterjs-content-model-core/lib/coreApi/setEditorStyle/setEditorStyle.ts b/packages/roosterjs-content-model-core/lib/coreApi/setEditorStyle/setEditorStyle.ts index 23255fa19b70..5db68b4a9103 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/setEditorStyle/setEditorStyle.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/setEditorStyle/setEditorStyle.ts @@ -21,7 +21,7 @@ export const setEditorStyle: SetEditorStyle = ( const doc = core.physicalRoot.ownerDocument; styleElement = doc.createElement('style'); - doc.head.appendChild(styleElement); + core.domHelper.appendToRoot(styleElement); styleElement.dataset.roosterjsStyleKey = key; core.lifecycle.styleElements[key] = styleElement; diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/areSameSelections.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/areSameSelections.ts index 7bb7d2414f16..25fdc12be604 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/areSameSelections.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/areSameSelections.ts @@ -1,3 +1,4 @@ +import { areSameRanges } from '../../utils/areSameRanges'; import type { CacheSelection, DOMSelection, @@ -59,7 +60,6 @@ const TableSelectionKeys: (keyof TableSelection)[] = [ 'firstRow', 'lastRow', ]; -const RangeKeys: (keyof Range)[] = ['startContainer', 'endContainer', 'startOffset', 'endOffset']; /** * @internal @@ -68,13 +68,6 @@ export function areSameTableSelections(t1: TableSelection, t2: TableSelection): return areSame(t1, t2, TableSelectionKeys); } -/** - * @internal - */ -export function areSameRanges(r1: Range, r2: Range): boolean { - return areSame(r1, r2, RangeKeys); -} - function isCacheSelection( sel: RangeSelectionForCache | RangeSelection ): sel is RangeSelectionForCache { diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts index 45faa6addcb4..c0e14d5f82ee 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts @@ -325,7 +325,19 @@ export class DomIndexerImpl implements DomIndexer { ); } else { const marker1 = this.reconcileNodeSelection(startContainer, startOffset); - const marker2 = this.reconcileNodeSelection(endContainer, endOffset); + // Pass marker1 to the second call so its adjacent-marker cleanup + // does not consume the SelectionMarker we just inserted. Without + // this guard, when marker1 lands directly next to endContainer's + // segment in paragraph.segments (e.g. startOffset == startContainer + // text length), the second splice would absorb marker1 and leave + // setSelection with a dangling reference. See issue #3341. + const marker2 = this.reconcileNodeSelection( + endContainer, + endOffset, + undefined, + undefined, + marker1 + ); if (marker1 && marker2) { if (newSelection.isReverted) { @@ -421,11 +433,18 @@ export class DomIndexerImpl implements DomIndexer { node: Node, offset: number, defaultFormat?: ContentModelSegmentFormat, - selectionMarker?: ContentModelSelectionMarker + selectionMarker?: ContentModelSelectionMarker, + preserveMarker?: Selectable ): Selectable | undefined { if (isNodeOfType(node, 'TEXT_NODE')) { if (isIndexedSegment(node)) { - return this.reconcileTextSelection(node, offset, undefined, selectionMarker); + return this.reconcileTextSelection( + node, + offset, + undefined, + selectionMarker, + preserveMarker + ); } else if (isIndexedDelimiter(node)) { return this.reconcileDelimiterSelection(node, defaultFormat); } else { @@ -462,7 +481,8 @@ export class DomIndexerImpl implements DomIndexer { textNode: IndexedSegmentNode, startOffset?: number, endOffset?: number, - selectionMarker?: ContentModelSelectionMarker + selectionMarker?: ContentModelSelectionMarker, + preserveMarker?: Selectable ) { const { paragraph, segments } = textNode.__roosterjsContentModel; const first = segments[0]; @@ -533,14 +553,16 @@ export class DomIndexerImpl implements DomIndexer { if (firstIndex >= 0 && lastIndex >= 0) { while ( firstIndex > 0 && - paragraph.segments[firstIndex - 1].segmentType == 'SelectionMarker' + paragraph.segments[firstIndex - 1].segmentType == 'SelectionMarker' && + paragraph.segments[firstIndex - 1] !== preserveMarker ) { firstIndex--; } while ( lastIndex < paragraph.segments.length - 1 && - paragraph.segments[lastIndex + 1].segmentType == 'SelectionMarker' + paragraph.segments[lastIndex + 1].segmentType == 'SelectionMarker' && + paragraph.segments[lastIndex + 1] !== preserveMarker ) { lastIndex++; } diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/lifecycle/LifecyclePlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/lifecycle/LifecyclePlugin.ts index c038e5776e77..2b4528734088 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/lifecycle/LifecyclePlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/lifecycle/LifecyclePlugin.ts @@ -85,6 +85,7 @@ class LifecyclePlugin implements PluginWithState<LifecyclePluginState> { // Initialize the Announce container. this.state.announceContainer = createAriaLiveElement(editor.getDocument()); + editor.getDOMHelper().appendToRoot(this.state.announceContainer); } /** diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts index 921fca1847f7..d25849d57613 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts @@ -736,19 +736,21 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> { private onSelectionChange = () => { if (this.editor?.hasFocus() && !this.editor.isInShadowEdit()) { const newSelection = this.editor.getDOMSelection(); + const domHelper = this.editor.getDOMHelper(); //If am image selection changed to a wider range due a keyboard event, we should update the selection - const selection = this.editor.getDocument().getSelection(); - if (selection && selection.focusNode) { - const image = isSingleImageInSelection(selection); + const range = domHelper.getSelectionRange(); + if (range) { + const image = isSingleImageInSelection(range); if (newSelection?.type == 'image' && !image) { - const range = selection.getRangeAt(0); + const sel = this.editor.getDocument().defaultView?.getSelection(); + const isReverted = sel + ? sel.focusNode != range.endContainer || sel.focusOffset != range.endOffset + : false; this.editor.setDOMSelection({ type: 'range', range, - isReverted: - selection.focusNode != range.endContainer || - selection.focusOffset != range.endOffset, + isReverted, }); } else if (newSelection?.type !== 'image' && image) { this.editor.setDOMSelection({ diff --git a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts index 016d5a8240b4..fcf9abf0ba8b 100644 --- a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts +++ b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts @@ -1,3 +1,4 @@ +import { areSameRanges } from '../../utils/areSameRanges'; import { getColor, getRangesByText, @@ -12,6 +13,18 @@ import type { DOMHelper, } from 'roosterjs-content-model-types'; +interface SelectionWithComposedRanges extends Selection { + getComposedRanges(options: { shadowRoots: ShadowRoot[] }): StaticRange[]; +} + +function isSelectionWithComposedRanges(sel: Selection): sel is SelectionWithComposedRanges { + return 'getComposedRanges' in sel; +} + +function isShadowRoot(node: Node): node is ShadowRoot { + return 'host' in node; +} + /** * @internal */ @@ -20,10 +33,26 @@ export interface DOMHelperImplOption { * @deprecated This is always treated as true now */ cloneIndependentRoot?: boolean; + + /** + * When true, enable shadow root detection so the editor works inside a Shadow DOM. + */ + useShadowDom?: boolean; } class DOMHelperImpl implements DOMHelper { - constructor(private contentDiv: HTMLElement, options?: DOMHelperImplOption) {} + private shadowRoot: ShadowRoot | null; + private doc: Document; + private useComposedRanges: boolean; + + constructor(private contentDiv: HTMLElement, options?: DOMHelperImplOption) { + const rootNode = contentDiv.getRootNode(); + this.shadowRoot = options?.useShadowDom && isShadowRoot(rootNode) ? rootNode : null; + this.doc = contentDiv.ownerDocument; + + const sel = this.doc.defaultView?.getSelection(); + this.useComposedRanges = !!(this.shadowRoot && sel && 'getComposedRanges' in sel); + } queryElements(selector: string): HTMLElement[] { return toArray(this.contentDiv.querySelectorAll(selector)) as HTMLElement[]; @@ -97,7 +126,9 @@ class DOMHelperImpl implements DOMHelper { } hasFocus(): boolean { - const activeElement = this.contentDiv.ownerDocument.activeElement; + const activeElement = this.shadowRoot + ? this.shadowRoot.activeElement + : this.doc.activeElement; return !!(activeElement && this.contentDiv.contains(activeElement)); } @@ -185,6 +216,53 @@ class DOMHelperImpl implements DOMHelper { getRangesByText(text: string, matchCase: boolean, wholeWord: boolean): Range[] { return getRangesByText(this.contentDiv, text, matchCase, wholeWord, true /*editableOnly*/); } + + getSelectionRange(): Range | null { + const sel = this.doc.defaultView?.getSelection(); + if (!sel) { + return null; + } + + if (this.useComposedRanges && this.shadowRoot && isSelectionWithComposedRanges(sel)) { + const staticRanges = sel.getComposedRanges({ + shadowRoots: [this.shadowRoot], + }); + + if (staticRanges?.length > 0) { + const sr = staticRanges[0]; + const range = this.doc.createRange(); + range.setStart(sr.startContainer, sr.startOffset); + range.setEnd(sr.endContainer, sr.endOffset); + return range; + } + return null; + } + + return sel.rangeCount > 0 ? sel.getRangeAt(0) : null; + } + + setSelectionRange(range: Range, isReverted: boolean = false): void { + const sel = this.doc.defaultView?.getSelection(); + const currentRange = this.getSelectionRange(); + if (!sel || (currentRange && areSameRanges(range, currentRange))) { + return; + } + + const { startContainer, startOffset, endContainer, endOffset } = range; + if (!isReverted) { + sel.setBaseAndExtent(startContainer, startOffset, endContainer, endOffset); + } else { + sel.setBaseAndExtent(endContainer, endOffset, startContainer, startOffset); + } + } + + appendToRoot(element: HTMLElement): void { + if (this.shadowRoot) { + this.shadowRoot.appendChild(element); + } else { + this.doc.body.appendChild(element); + } + } } /** diff --git a/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts b/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts index 96398a35276d..0f95a9216cca 100644 --- a/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts +++ b/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts @@ -50,7 +50,11 @@ export function createEditorCore(contentDiv: HTMLDivElement, options: EditorOpti ? options.trustedHTMLHandler : createTrustedHTMLHandler(domCreator), domCreator: domCreator, - domHelper: createDOMHelper(contentDiv), + domHelper: createDOMHelper(contentDiv, { + useShadowDom: + !!options.experimentalFeatures && + options.experimentalFeatures.indexOf('ShadowDom') >= 0, + }), ...getPluginState(corePlugins), disposeErrorHandler: options.disposeErrorHandler, onFixUpModel: options.onFixUpModel, diff --git a/packages/roosterjs-content-model-core/lib/override/containerSizeFormatParser.ts b/packages/roosterjs-content-model-core/lib/override/containerSizeFormatParser.ts index 077215150284..1b486148dd73 100644 --- a/packages/roosterjs-content-model-core/lib/override/containerSizeFormatParser.ts +++ b/packages/roosterjs-content-model-core/lib/override/containerSizeFormatParser.ts @@ -8,5 +8,7 @@ export const containerSizeFormatParser: FormatParser<SizeFormat> = (format, elem if (element.tagName == 'DIV' || element.tagName == 'P') { delete format.width; delete format.height; + delete format.maxHeight; + delete format.maxWidth; } }; diff --git a/packages/roosterjs-content-model-core/lib/utils/areSameRanges.ts b/packages/roosterjs-content-model-core/lib/utils/areSameRanges.ts new file mode 100644 index 000000000000..ec2871635934 --- /dev/null +++ b/packages/roosterjs-content-model-core/lib/utils/areSameRanges.ts @@ -0,0 +1,9 @@ +const RangeKeys: (keyof Range)[] = ['startContainer', 'endContainer', 'startOffset', 'endOffset']; + +/** + * @internal + * Check if two ranges have the same start and end positions. + */ +export function areSameRanges(r1: Range, r2: Range): boolean { + return RangeKeys.every(k => r1[k] == r2[k]); +} diff --git a/packages/roosterjs-content-model-core/lib/utils/createAriaLiveElement.ts b/packages/roosterjs-content-model-core/lib/utils/createAriaLiveElement.ts index e255a8d7551f..60edaa9b430f 100644 --- a/packages/roosterjs-content-model-core/lib/utils/createAriaLiveElement.ts +++ b/packages/roosterjs-content-model-core/lib/utils/createAriaLiveElement.ts @@ -13,7 +13,5 @@ export function createAriaLiveElement(document: Document): HTMLDivElement { div.style.width = '1px'; div.ariaLive = 'assertive'; - document.body.appendChild(div); - return div; } diff --git a/packages/roosterjs-content-model-core/test/command/cutCopy/getContentForCopyTest.ts b/packages/roosterjs-content-model-core/test/command/cutCopy/getContentForCopyTest.ts index 5207fb2861d9..dbde0cb2d79d 100644 --- a/packages/roosterjs-content-model-core/test/command/cutCopy/getContentForCopyTest.ts +++ b/packages/roosterjs-content-model-core/test/command/cutCopy/getContentForCopyTest.ts @@ -295,4 +295,42 @@ describe('getContentForCopy', () => { expect(result).toBeDefined(); div.remove(); }); + + it('should build text content from the clonedRoot modified by beforeCutCopy', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const text = createText('original text'); + + text.isSelected = true; + para.segments.push(text); + model.blocks.push(para); + + const div = mockDocument.createElement('div'); + mockDocument.body.appendChild(div); + const range: Range = new Range(); + range.selectNode(div); + + const selection: DOMSelection = { + type: 'range', + range, + isReverted: false, + }; + + spyOn(editor, 'getDOMSelection').and.returnValue(selection); + spyOn(editor, 'getContentModelCopy').and.returnValue(model); + + // Event handler replaces the content of the cloned root tree + triggerEventSpy.and.callFake((_eventType: string, event: BeforeCutCopyEvent) => { + const modifiedRoot = event.clonedRoot; + modifiedRoot.innerHTML = '<p>modified text</p>'; + + return event; + }); + + const result = getContentForCopy(editor, false, new ClipboardEvent('copy')); + + expect(result).not.toBeNull(); + expect(result!.textContent).toBe('modified text'); + div.remove(); + }); }); diff --git a/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts b/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts index 2e6938eff6ab..c66545d8b1ef 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts @@ -211,15 +211,17 @@ describe('retrieveHtmlInfo', () => { text: 'color: red;', }, { - selectors: ['.b div', ' .c'], + selectors: ['.b div', '.c'], text: 'font-size: 10pt;', }, { selectors: ['test'], - text: 'border: none;', + text: + 'border-width: medium; border-style: none; border-color: currentcolor; border-image: initial;', }, ], metadata: {}, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['DIV'], diff --git a/packages/roosterjs-content-model-core/test/coreApi/announce/announceTest.ts b/packages/roosterjs-content-model-core/test/coreApi/announce/announceTest.ts index 516a634b7160..59dd6a908cf2 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/announce/announceTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/announce/announceTest.ts @@ -1,15 +1,16 @@ import { announce } from '../../../lib/coreApi/announce/announce'; +import { createMockDomHelper } from '../../testUtils/createMockDomHelper'; import { EditorCore } from 'roosterjs-content-model-types'; describe('announce', () => { let core: EditorCore; let createElementSpy: jasmine.Spy; - let appendChildSpy: jasmine.Spy; let getterSpy: jasmine.Spy; + let mockDomHelper: ReturnType<typeof createMockDomHelper>; beforeEach(() => { + mockDomHelper = createMockDomHelper(); createElementSpy = jasmine.createSpy('createElement'); - appendChildSpy = jasmine.createSpy('appendChild'); getterSpy = jasmine.createSpy('getter'); core = { @@ -19,11 +20,9 @@ describe('announce', () => { physicalRoot: { ownerDocument: { createElement: createElementSpy, - body: { - appendChild: appendChildSpy, - }, }, }, + domHelper: mockDomHelper, } as any; }); @@ -36,7 +35,7 @@ describe('announce', () => { announce(core, {}); expect(createElementSpy).toHaveBeenCalled(); - expect(appendChildSpy).toHaveBeenCalled(); + expect(mockDomHelper.appendToRoot).toHaveBeenCalled(); expect(mockedDiv.textContent).toBeUndefined(); }); @@ -51,7 +50,7 @@ describe('announce', () => { }); expect(createElementSpy).toHaveBeenCalledWith('div'); - expect(appendChildSpy).toHaveBeenCalledWith(mockedDiv); + expect(mockDomHelper.appendToRoot).toHaveBeenCalledWith(mockedDiv); expect(mockedDiv).toEqual({ style: { clip: 'rect(0px, 0px, 0px, 0px)', @@ -81,7 +80,7 @@ describe('announce', () => { expect(getterSpy).toHaveBeenCalledWith('announceListItemBullet'); expect(createElementSpy).toHaveBeenCalledWith('div'); - expect(appendChildSpy).toHaveBeenCalledWith(mockedDiv); + expect(mockDomHelper.appendToRoot).toHaveBeenCalledWith(mockedDiv); expect(mockedDiv).toEqual({ style: { clip: 'rect(0px, 0px, 0px, 0px)', @@ -112,7 +111,7 @@ describe('announce', () => { expect(getterSpy).toHaveBeenCalledWith('announceListItemBullet'); expect(createElementSpy).toHaveBeenCalledWith('div'); - expect(appendChildSpy).toHaveBeenCalledWith(mockedDiv); + expect(mockDomHelper.appendToRoot).toHaveBeenCalledWith(mockedDiv); expect(mockedDiv).toEqual({ style: { clip: 'rect(0px, 0px, 0px, 0px)', @@ -143,7 +142,7 @@ describe('announce', () => { expect(getterSpy).toHaveBeenCalledWith('announceListItemBullet'); expect(createElementSpy).toHaveBeenCalledWith('div'); - expect(appendChildSpy).toHaveBeenCalledWith(mockedDiv); + expect(mockDomHelper.appendToRoot).toHaveBeenCalledWith(mockedDiv); expect(mockedDiv).toEqual({ style: { clip: 'rect(0px, 0px, 0px, 0px)', @@ -177,7 +176,7 @@ describe('announce', () => { expect(removeChildSpy).not.toHaveBeenCalled(); expect(createElementSpy).not.toHaveBeenCalled(); - expect(appendChildSpy).not.toHaveBeenCalled(); + expect(mockDomHelper.appendToRoot).not.toHaveBeenCalled(); expect(mockedDiv).toEqual({ textContent: 'test', parentElement: { diff --git a/packages/roosterjs-content-model-core/test/coreApi/getDOMSelection/getDOMSelectionTest.ts b/packages/roosterjs-content-model-core/test/coreApi/getDOMSelection/getDOMSelectionTest.ts index b3841ece03c5..af5219d91658 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/getDOMSelection/getDOMSelectionTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/getDOMSelection/getDOMSelectionTest.ts @@ -1,3 +1,4 @@ +import { createMockDomHelper } from '../../testUtils/createMockDomHelper'; import { EditorCore } from 'roosterjs-content-model-types'; import { getDOMSelection } from '../../../lib/coreApi/getDOMSelection/getDOMSelection'; @@ -26,9 +27,13 @@ describe('getDOMSelection', () => { logicalRoot: contentDiv, lifecycle: {}, selection: {}, - domHelper: { + domHelper: createMockDomHelper({ hasFocus: hasFocusSpy, - }, + getSelectionRange: jasmine.createSpy('getSelectionRange').and.callFake(() => { + const selection = getSelectionSpy(); + return selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null; + }), + }), } as any; }); diff --git a/packages/roosterjs-content-model-core/test/coreApi/setDOMSelection/setDOMSelectionTest.ts b/packages/roosterjs-content-model-core/test/coreApi/setDOMSelection/setDOMSelectionTest.ts index ee18692d5e84..2a16cd2a68a1 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/setDOMSelection/setDOMSelectionTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/setDOMSelection/setDOMSelectionTest.ts @@ -1,4 +1,4 @@ -import * as addRangeToSelection from '../../../lib/coreApi/setDOMSelection/addRangeToSelection'; +import { createMockDomHelper } from '../../testUtils/createMockDomHelper'; import { DOMSelection, EditorCore } from 'roosterjs-content-model-types'; import { setDOMSelection } from '../../../lib/coreApi/setDOMSelection/setDOMSelection'; @@ -13,10 +13,11 @@ describe('setDOMSelection', () => { let querySelectorAllSpy: jasmine.Spy; let hasFocusSpy: jasmine.Spy; let triggerEventSpy: jasmine.Spy; - let addRangeToSelectionSpy: jasmine.Spy; + let setSelectionRangeSpy: jasmine.Spy; let createRangeSpy: jasmine.Spy; let setEditorStyleSpy: jasmine.Spy; let containsSpy: jasmine.Spy; + let isNodeInEditorSpy: jasmine.Spy; let doc: Document; let contentDiv: HTMLDivElement; let mockedRange = 'RANGE' as any; @@ -28,14 +29,13 @@ describe('setDOMSelection', () => { querySelectorAllSpy = jasmine.createSpy('querySelectorAll'); hasFocusSpy = jasmine.createSpy('hasFocus'); triggerEventSpy = jasmine.createSpy('triggerEvent'); - addRangeToSelectionSpy = spyOn(addRangeToSelection, 'addRangeToSelection').and.callFake( - () => { - expect(core.selection.skipReselectOnFocus).toBeTrue(); - } - ); + setSelectionRangeSpy = jasmine.createSpy('setSelectionRange').and.callFake(() => { + expect(core.selection.skipReselectOnFocus).toBeTrue(); + }); createRangeSpy = jasmine.createSpy('createRange'); setEditorStyleSpy = jasmine.createSpy('setEditorStyle'); containsSpy = jasmine.createSpy('contains').and.returnValue(true); + isNodeInEditorSpy = jasmine.createSpy('isNodeInEditor').and.returnValue(true); appendChildSpy = jasmine.createSpy('appendChild'); createElementSpy = jasmine.createSpy('createElement').and.returnValue({ appendChild: appendChildSpy, @@ -47,9 +47,18 @@ describe('setDOMSelection', () => { createRange: createRangeSpy, contains: containsSpy, createElement: createElementSpy, + defaultView: { + getSelection: () => ({ + rangeCount: 1, + getRangeAt: () => mockedRange, + focusNode: mockedRange.endContainer, + focusOffset: mockedRange.endOffset, + }), + }, } as any; contentDiv = { ownerDocument: doc, + contains: containsSpy, } as any; core = { @@ -64,9 +73,11 @@ describe('setDOMSelection', () => { setEditorStyle: setEditorStyleSpy, getDOMSelection: getDOMSelectionSpy, }, - domHelper: { + domHelper: createMockDomHelper({ hasFocus: hasFocusSpy, - }, + setSelectionRange: setSelectionRangeSpy, + isNodeInEditor: isNodeInEditorSpy, + }), lifecycle: { isDarkMode: false, }, @@ -100,7 +111,7 @@ describe('setDOMSelection', () => { }, true ); - expect(addRangeToSelectionSpy).not.toHaveBeenCalled(); + expect(setSelectionRangeSpy).not.toHaveBeenCalled(); expect(setEditorStyleSpy).toHaveBeenCalledTimes(3); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelectionHideCursor', null); @@ -177,11 +188,7 @@ describe('setDOMSelection', () => { }, true ); - expect(addRangeToSelectionSpy).toHaveBeenCalledWith( - doc, - mockedRange, - false /* isReverted */ - ); + expect(setSelectionRangeSpy).toHaveBeenCalledWith(mockedRange, false /* isReverted */); }); it('range selection, editor id is unique, editor has focus, do not trigger event', () => { @@ -203,7 +210,7 @@ describe('setDOMSelection', () => { tableCellSelectionBackgroundColor: DEFAULT_TABLE_CELL_SELECTION_BACKGROUND_COLOR, } as any); expect(triggerEventSpy).not.toHaveBeenCalled(); - expect(addRangeToSelectionSpy).toHaveBeenCalledWith(doc, mockedRange, false); + expect(setSelectionRangeSpy).toHaveBeenCalledWith(mockedRange, false); expect(setEditorStyleSpy).toHaveBeenCalledTimes(3); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelectionHideCursor', null); @@ -240,7 +247,7 @@ describe('setDOMSelection', () => { }, true ); - expect(addRangeToSelectionSpy).toHaveBeenCalledWith(doc, mockedRange, false); + expect(setSelectionRangeSpy).toHaveBeenCalledWith(mockedRange, false); expect(setEditorStyleSpy).toHaveBeenCalledTimes(3); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelectionHideCursor', null); @@ -263,6 +270,7 @@ describe('setDOMSelection', () => { appendChild: appendChildSpy, }, ownerDocument: doc, + getRootNode: () => doc, } as any; }); @@ -301,7 +309,7 @@ describe('setDOMSelection', () => { ); expect(selectNodeSpy).toHaveBeenCalledWith(mockedImage); expect(collapseSpy).not.toHaveBeenCalledWith(); - expect(addRangeToSelectionSpy).toHaveBeenCalledWith(doc, mockedRange, undefined); + expect(setSelectionRangeSpy).toHaveBeenCalledWith(mockedRange, false); expect(mockedImage.id).toBe('image_0'); expect(setEditorStyleSpy).toHaveBeenCalledTimes(5); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); @@ -362,7 +370,7 @@ describe('setDOMSelection', () => { ); expect(selectNodeSpy).toHaveBeenCalledWith(mockedImage); expect(collapseSpy).not.toHaveBeenCalledWith(); - expect(addRangeToSelectionSpy).toHaveBeenCalledWith(doc, mockedRange, undefined); + expect(setSelectionRangeSpy).toHaveBeenCalledWith(mockedRange, false); expect(setEditorStyleSpy).toHaveBeenCalledTimes(5); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelectionHideCursor', null); @@ -425,7 +433,7 @@ describe('setDOMSelection', () => { ); expect(selectNodeSpy).toHaveBeenCalledWith(mockedImage); expect(collapseSpy).not.toHaveBeenCalledWith(); - expect(addRangeToSelectionSpy).toHaveBeenCalledWith(doc, mockedRange, undefined); + expect(setSelectionRangeSpy).toHaveBeenCalledWith(mockedRange, false); expect(setEditorStyleSpy).toHaveBeenCalledTimes(5); expect(setEditorStyleSpy).toHaveBeenCalledWith(coreValue, '_DOMSelection', null); expect(setEditorStyleSpy).toHaveBeenCalledWith( @@ -444,6 +452,7 @@ describe('setDOMSelection', () => { 'outline-style:solid!important; outline-color:DarkColorMock-red!important;', ['#image_0'] ); + expect(setSelectionRangeSpy).toHaveBeenCalledWith(mockedRange, false); expect(setEditorStyleSpy).toHaveBeenCalledWith( coreValue, '_DOMSelectionHideSelection', @@ -464,7 +473,8 @@ describe('setDOMSelection', () => { collapse: collapseSpy, }; - doc.contains = () => false; + containsSpy.and.returnValue(false); + isNodeInEditorSpy.and.returnValue(false); createRangeSpy.and.returnValue(mockedRange); @@ -489,7 +499,7 @@ describe('setDOMSelection', () => { ); expect(selectNodeSpy).not.toHaveBeenCalled(); expect(collapseSpy).not.toHaveBeenCalled(); - expect(addRangeToSelectionSpy).not.toHaveBeenCalled(); + expect(setSelectionRangeSpy).not.toHaveBeenCalled(); expect(mockedImage.id).toBe('image_0'); expect(setEditorStyleSpy).toHaveBeenCalledTimes(5); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); @@ -551,7 +561,7 @@ describe('setDOMSelection', () => { ); expect(selectNodeSpy).toHaveBeenCalledWith(mockedImage); expect(collapseSpy).not.toHaveBeenCalledWith(); - expect(addRangeToSelectionSpy).toHaveBeenCalledWith(doc, mockedRange, undefined); + expect(setSelectionRangeSpy).toHaveBeenCalledWith(mockedRange, false); expect(setEditorStyleSpy).toHaveBeenCalledTimes(5); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelectionHideCursor', null); @@ -612,7 +622,7 @@ describe('setDOMSelection', () => { ); expect(selectNodeSpy).toHaveBeenCalledWith(mockedImage); expect(collapseSpy).not.toHaveBeenCalledWith(); - expect(addRangeToSelectionSpy).toHaveBeenCalledWith(doc, mockedRange, undefined); + expect(setSelectionRangeSpy).toHaveBeenCalledWith(mockedRange, false); expect(setEditorStyleSpy).toHaveBeenCalledTimes(5); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelectionHideCursor', null); @@ -671,7 +681,7 @@ describe('setDOMSelection', () => { ); expect(selectNodeSpy).toHaveBeenCalledWith(mockedImage); expect(collapseSpy).not.toHaveBeenCalledWith(); - expect(addRangeToSelectionSpy).toHaveBeenCalledWith(doc, mockedRange, undefined); + expect(setSelectionRangeSpy).toHaveBeenCalledWith(mockedRange, false); expect(mockedImage.id).toBe('0'); expect(setEditorStyleSpy).toHaveBeenCalledTimes(5); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); @@ -704,6 +714,7 @@ describe('setDOMSelection', () => { ownerDocument: doc, rows: [], childNodes: [], + getRootNode: () => doc, } as any; }); @@ -734,7 +745,7 @@ describe('setDOMSelection', () => { expect(triggerEventSpy).not.toHaveBeenCalled(); expect(selectNodeSpy).not.toHaveBeenCalled(); expect(collapseSpy).not.toHaveBeenCalled(); - expect(addRangeToSelectionSpy).not.toHaveBeenCalled(); + expect(setSelectionRangeSpy).not.toHaveBeenCalled(); expect(mockedTable.id).toBeUndefined(); expect(setEditorStyleSpy).toHaveBeenCalledTimes(3); @@ -912,8 +923,7 @@ describe('setDOMSelection', () => { '#table_0>TBODY> tr:nth-child(1)>TD:nth-child(2) *', ]); - expect(containsSpy).toHaveBeenCalledTimes(1); - expect(containsSpy).toHaveBeenCalledWith(innerDIV); + expect(isNodeInEditorSpy).toHaveBeenCalledWith(innerDIV); }); it('Select TD with double merged cell', () => { @@ -1120,7 +1130,7 @@ describe('setDOMSelection', () => { }, true ); - expect(addRangeToSelectionSpy).not.toHaveBeenCalled(); + expect(setSelectionRangeSpy).not.toHaveBeenCalled(); expect(setEditorStyleSpy).toHaveBeenCalledTimes(3); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); expect(setEditorStyleSpy).toHaveBeenCalledWith( @@ -1135,7 +1145,7 @@ describe('setDOMSelection', () => { ); } else { expect(triggerEventSpy).not.toHaveBeenCalled(); - expect(addRangeToSelectionSpy).not.toHaveBeenCalled(); + expect(setSelectionRangeSpy).not.toHaveBeenCalled(); expect(setEditorStyleSpy).not.toHaveBeenCalled(); } } @@ -1181,6 +1191,7 @@ describe('setDOMSelection', () => { appendChild: appendChildSpy, }, ownerDocument: doc, + getRootNode: () => doc, } as any; runTest( diff --git a/packages/roosterjs-content-model-core/test/coreApi/setEditorStyle/ensureUniqueIdTest.ts b/packages/roosterjs-content-model-core/test/coreApi/setEditorStyle/ensureUniqueIdTest.ts index 499ceee73fbd..70d1e7a784ea 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/setEditorStyle/ensureUniqueIdTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/setEditorStyle/ensureUniqueIdTest.ts @@ -1,5 +1,13 @@ import { ensureUniqueId } from '../../../lib/coreApi/setEditorStyle/ensureUniqueId'; +function createMockElement(mock: Partial<HTMLElement>, doc: Document) { + return { + ownerDocument: doc, + getRootNode: () => doc, + ...mock, + } as HTMLElement; +} + describe('ensureUniqueId', () => { let doc: Document; let querySelectorAllSpy: jasmine.Spy; @@ -12,9 +20,7 @@ describe('ensureUniqueId', () => { }); it('no id', () => { - const element = { - ownerDocument: doc, - } as any; + const element = createMockElement({}, doc); querySelectorAllSpy.and.returnValue([]); const result = ensureUniqueId(element, 'prefix'); @@ -22,10 +28,12 @@ describe('ensureUniqueId', () => { }); it('Has unique id', () => { - const element = { - ownerDocument: doc, - id: 'unique', - } as any; + const element = createMockElement( + { + id: 'unique', + }, + doc + ); querySelectorAllSpy.and.returnValue([{}]); const result = ensureUniqueId(element, 'prefix'); @@ -33,10 +41,12 @@ describe('ensureUniqueId', () => { }); it('Has duplicated', () => { - const element = { - ownerDocument: doc, - id: 'dup', - } as any; + const element = createMockElement( + { + id: 'dup', + }, + doc + ); querySelectorAllSpy.and.callFake((selector: string) => selector == '#dup' ? [{}, {}] : [] ); @@ -46,10 +56,12 @@ describe('ensureUniqueId', () => { }); it('Has duplicated and unsupported id', () => { - const element = { - ownerDocument: doc, - id: '0dup', - } as any; + const element = createMockElement( + { + id: '0dup', + }, + doc + ); querySelectorAllSpy.and.callFake((selector: string) => selector == '[id="0dup"]' ? [{}, {}] : [] ); @@ -59,10 +71,12 @@ describe('ensureUniqueId', () => { }); it('Should not throw when element id starts with number', () => { - const element = { - ownerDocument: doc, - id: '0', - } as any; + const element = createMockElement( + { + id: '0', + }, + doc + ); let isFirst = true; querySelectorAllSpy.and.callFake((_selector: string) => { @@ -80,10 +94,12 @@ describe('ensureUniqueId', () => { }); it('Should not throw when element id starts with hyphen', () => { - const element = { - ownerDocument: doc, - id: '-', - } as any; + const element = createMockElement( + { + id: '-', + }, + doc + ); let isFirst = true; querySelectorAllSpy.and.callFake((_selector: string) => { diff --git a/packages/roosterjs-content-model-core/test/coreApi/setEditorStyle/setEditorStyleTest.ts b/packages/roosterjs-content-model-core/test/coreApi/setEditorStyle/setEditorStyleTest.ts index 2c9492f8d5d7..140a4d2346bc 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/setEditorStyle/setEditorStyleTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/setEditorStyle/setEditorStyleTest.ts @@ -1,19 +1,20 @@ import * as ensureUniqueId from '../../../lib/coreApi/setEditorStyle/ensureUniqueId'; +import { createMockDomHelper } from '../../testUtils/createMockDomHelper'; import { EditorCore } from 'roosterjs-content-model-types'; import { setEditorStyle } from '../../../lib/coreApi/setEditorStyle/setEditorStyle'; describe('setEditorStyle', () => { let core: EditorCore; let createElementSpy: jasmine.Spy; - let appendChildSpy: jasmine.Spy; let insertRuleSpy: jasmine.Spy; let deleteRuleSpy: jasmine.Spy; let ensureUniqueIdSpy: jasmine.Spy; let mockedStyle: HTMLStyleElement; + let mockDomHelper: ReturnType<typeof createMockDomHelper>; beforeEach(() => { + mockDomHelper = createMockDomHelper(); createElementSpy = jasmine.createSpy('createElement'); - appendChildSpy = jasmine.createSpy('appendChild'); insertRuleSpy = jasmine.createSpy('insertRule'); deleteRuleSpy = jasmine.createSpy('deleteRule'); ensureUniqueIdSpy = spyOn(ensureUniqueId, 'ensureUniqueId').and.returnValue('uniqueId'); @@ -21,11 +22,9 @@ describe('setEditorStyle', () => { physicalRoot: { ownerDocument: { createElement: createElementSpy, - head: { - appendChild: appendChildSpy, - }, }, }, + domHelper: mockDomHelper, lifecycle: { styleElements: {}, }, @@ -46,7 +45,7 @@ describe('setEditorStyle', () => { expect(core.lifecycle.styleElements).toEqual({}); expect(createElementSpy).not.toHaveBeenCalled(); - expect(appendChildSpy).not.toHaveBeenCalled(); + expect(mockDomHelper.appendToRoot).not.toHaveBeenCalled(); expect(insertRuleSpy).not.toHaveBeenCalled(); expect(deleteRuleSpy).not.toHaveBeenCalled(); expect(ensureUniqueIdSpy).not.toHaveBeenCalled(); @@ -60,7 +59,7 @@ describe('setEditorStyle', () => { setEditorStyle(core, 'key0', 'rule'); expect(createElementSpy).toHaveBeenCalledWith('style'); - expect(appendChildSpy).toHaveBeenCalledWith(mockedStyle); + expect(mockDomHelper.appendToRoot).toHaveBeenCalledWith(mockedStyle); expect(insertRuleSpy).toHaveBeenCalledTimes(1); expect(insertRuleSpy).toHaveBeenCalledWith('#uniqueId {rule}'); expect(deleteRuleSpy).not.toHaveBeenCalled(); @@ -77,7 +76,7 @@ describe('setEditorStyle', () => { setEditorStyle(core, 'key0', 'rule', ['selector1', 'selector2']); expect(createElementSpy).toHaveBeenCalledWith('style'); - expect(appendChildSpy).toHaveBeenCalledWith(mockedStyle); + expect(mockDomHelper.appendToRoot).toHaveBeenCalledWith(mockedStyle); expect(insertRuleSpy).toHaveBeenCalledTimes(1); expect(insertRuleSpy).toHaveBeenCalledWith( '#uniqueId selector1,#uniqueId selector2 {rule}' @@ -96,7 +95,7 @@ describe('setEditorStyle', () => { setEditorStyle(core, 'key0', 'rule', 'before'); expect(createElementSpy).toHaveBeenCalledWith('style'); - expect(appendChildSpy).toHaveBeenCalledWith(mockedStyle); + expect(mockDomHelper.appendToRoot).toHaveBeenCalledWith(mockedStyle); expect(insertRuleSpy).toHaveBeenCalledTimes(1); expect(insertRuleSpy).toHaveBeenCalledWith('#uniqueId::before {rule}'); expect(deleteRuleSpy).not.toHaveBeenCalled(); @@ -127,7 +126,7 @@ describe('setEditorStyle', () => { setEditorStyle(core, 'key0', null); expect(createElementSpy).not.toHaveBeenCalled(); - expect(appendChildSpy).not.toHaveBeenCalled(); + expect(mockDomHelper.appendToRoot).not.toHaveBeenCalled(); expect(insertRuleSpy).toHaveBeenCalledTimes(0); expect(deleteRuleSpy).toHaveBeenCalledTimes(2); expect(deleteRuleSpy).toHaveBeenCalledWith(1); @@ -165,7 +164,7 @@ describe('setEditorStyle', () => { setEditorStyle(core, 'key0', 'rule3'); expect(createElementSpy).not.toHaveBeenCalled(); - expect(appendChildSpy).not.toHaveBeenCalled(); + expect(mockDomHelper.appendToRoot).not.toHaveBeenCalled(); expect(insertRuleSpy).toHaveBeenCalledTimes(1); expect(insertRuleSpy).toHaveBeenCalledWith('#uniqueId {rule3}'); expect(deleteRuleSpy).toHaveBeenCalledTimes(2); @@ -195,7 +194,7 @@ describe('setEditorStyle', () => { setEditorStyle(core, 'key0', 'rule', selectors, 50); expect(createElementSpy).toHaveBeenCalledWith('style'); - expect(appendChildSpy).toHaveBeenCalledWith(mockedStyle); + expect(mockDomHelper.appendToRoot).toHaveBeenCalledWith(mockedStyle); expect(insertRuleSpy).toHaveBeenCalledTimes(3); expect(insertRuleSpy).toHaveBeenCalledWith( '#uniqueId longSelector1,#uniqueId longSelector2 {rule}' diff --git a/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts index fc9b7f70e05e..03645f9b4926 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts @@ -605,6 +605,74 @@ describe('domIndexerImpl.reconcileSelection', () => { expect(model.hasRevertedRangeSelection).toBeFalsy(); }); + it('Repro #3341: range across two text nodes with startOffset at end of first node', () => { + const node1 = document.createTextNode('test1') as any; + const node2 = document.createTextNode('test2') as any; + const parent = document.createElement('div'); + + parent.appendChild(node1); + parent.appendChild(node2); + + // Range starts at the END of node1 (offset 5) and ends inside node2 (offset 3). + // After the first reconcile call marker1 lands directly before node2's segment; + // the second call's adjacent-marker cleanup loop must NOT eat marker1. + const newRangeEx: DOMSelection = { + type: 'range', + range: createRange(node1, 5, node2, 3), + isReverted: false, + }; + const paragraph = createParagraph(); + const oldSegment1 = createText(''); + const oldSegment2 = createText(''); + + paragraph.segments.push(oldSegment1, oldSegment2); + domIndexerImpl.onSegment(node1, paragraph, [oldSegment1]); + domIndexerImpl.onSegment(node2, paragraph, [oldSegment2]); + model.blocks.push(paragraph); + + const result = domIndexerImpl.reconcileSelection(model, newRangeEx); + + const segment1: ContentModelSegment = { + segmentType: 'Text', + text: 'test1', + format: {}, + }; + const segment2: ContentModelSegment = { + segmentType: 'Text', + text: 'tes', + format: {}, + isSelected: true, + }; + const segment3: ContentModelSegment = { + segmentType: 'Text', + text: 't2', + format: {}, + }; + const marker1 = createSelectionMarker(); + const marker2 = createSelectionMarker(); + + expect(result).toBeTrue(); + expect(node1.__roosterjsContentModel).toEqual({ + paragraph, + segments: [segment1], + }); + expect(node2.__roosterjsContentModel).toEqual({ + paragraph, + segments: [segment2, segment3], + }); + expect(paragraph).toEqual({ + blockType: 'Paragraph', + format: {}, + segments: [segment1, marker1, segment2, marker2, segment3], + }); + expect(setSelectionSpy).toHaveBeenCalledWith(model, marker1, marker2); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [paragraph], + }); + expect(model.hasRevertedRangeSelection).toBeFalsy(); + }); + it('no old range, normal range on indexed text, expanded on other type of node', () => { const node1 = document.createTextNode('test1') as any; const node2 = document.createElement('br') as any; diff --git a/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/CopyPastePluginTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/CopyPastePluginTest.ts index 8589b1782d51..1ee6ccaa9845 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/CopyPastePluginTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/CopyPastePluginTest.ts @@ -1,4 +1,3 @@ -import * as addRangeToSelection from '../../../lib/coreApi/setDOMSelection/addRangeToSelection'; import * as contentModelToDomFile from 'roosterjs-content-model-dom/lib/modelToDom/contentModelToDom'; import * as copyPasteEntityOverride from '../../../lib/override/pasteCopyBlockEntityParser'; import * as deleteSelectionsFile from 'roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection'; @@ -126,8 +125,6 @@ describe('CopyPastePlugin |', () => { }); }); - spyOn(addRangeToSelection, 'addRangeToSelection'); - plugin = createCopyPastePlugin({ allowedCustomPasteType, }); @@ -532,7 +529,7 @@ describe('CopyPastePlugin |', () => { 'text/html', '<img id="image">' ); - expect(event.clipboardData?.setData).toHaveBeenCalledWith('text/plain', ''); + expect(event.clipboardData?.setData).toHaveBeenCalledWith('text/plain', ' '); // On Cut Spy expect(formatContentModelSpy).not.toHaveBeenCalled(); @@ -956,7 +953,7 @@ describe('CopyPastePlugin |', () => { 'text/html', '<img id="image">' ); - expect(event.clipboardData?.setData).toHaveBeenCalledWith('text/plain', ''); + expect(event.clipboardData?.setData).toHaveBeenCalledWith('text/plain', ' '); // On Cut Spy expect(formatContentModelSpy).toHaveBeenCalledTimes(1); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/lifecycle/LifecyclePluginTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/lifecycle/LifecyclePluginTest.ts index a1981b030773..35d7c3f2be88 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/lifecycle/LifecyclePluginTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/lifecycle/LifecyclePluginTest.ts @@ -7,7 +7,14 @@ import { DarkColorHandler, IEditor } from 'roosterjs-content-model-types'; const announceContainer = {} as Readonly<HTMLDivElement>; describe('LifecyclePlugin', () => { + let appendToRootSpy: jasmine.Spy; + let getDOMHelper: jasmine.Spy; + beforeEach(() => { + appendToRootSpy = jasmine.createSpy('appendToRoot'); + getDOMHelper = jasmine.createSpy('getDOMHelper').and.returnValue({ + appendToRoot: appendToRootSpy, + }); spyOn(createAriaLiveElementFile, 'createAriaLiveElement').and.returnValue( announceContainer ); @@ -27,6 +34,7 @@ describe('LifecyclePlugin', () => { getColorManager: () => <DarkColorHandler | null>null, isDarkMode: () => false, getDocument, + getDOMHelper, })); expect(state).toEqual({ @@ -75,6 +83,7 @@ describe('LifecyclePlugin', () => { getColorManager: () => <DarkColorHandler | null>null, isDarkMode: () => false, getDocument, + getDOMHelper, })); expect(state).toEqual({ @@ -119,6 +128,7 @@ describe('LifecyclePlugin', () => { getColorManager: () => <DarkColorHandler | null>null, isDarkMode: () => false, getDocument, + getDOMHelper, })); expect(state).toEqual({ @@ -156,6 +166,7 @@ describe('LifecyclePlugin', () => { getColorManager: () => <DarkColorHandler | null>null, isDarkMode: () => false, getDocument, + getDOMHelper, })); expect(div.isContentEditable).toBeTrue(); @@ -184,6 +195,7 @@ describe('LifecyclePlugin', () => { getColorManager: () => <DarkColorHandler | null>null, isDarkMode: () => false, getDocument, + getDOMHelper, })); expect(div.isContentEditable).toBeFalse(); @@ -213,6 +225,7 @@ describe('LifecyclePlugin', () => { triggerEvent, getColorManager: () => mockedDarkColorHandler, getDocument, + getDOMHelper, })); expect(setColorSpy).toHaveBeenCalledTimes(2); @@ -248,6 +261,7 @@ describe('LifecyclePlugin', () => { triggerEvent, getColorManager: () => mockedDarkColorHandler, getDocument, + getDOMHelper, })); expect(setColorSpy).toHaveBeenCalledTimes(2); @@ -305,6 +319,7 @@ describe('LifecyclePlugin', () => { triggerEvent, getColorManager: () => mockedDarkColorHandler, getDocument, + getDOMHelper, })); expect(setColorSpy).toHaveBeenCalledTimes(0); @@ -338,6 +353,7 @@ describe('LifecyclePlugin', () => { getColorManager: jasmine.createSpy(), triggerEvent: jasmine.createSpy(), getDocument, + getDOMHelper, }); const state = plugin.getState(); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts index 203e8555db99..42357b2f8e2f 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts @@ -3239,6 +3239,7 @@ describe('SelectionPlugin on Safari', () => { let hasFocusSpy: jasmine.Spy; let isInShadowEditSpy: jasmine.Spy; let getDOMSelectionSpy: jasmine.Spy; + let getDOMHelperSpy: jasmine.Spy; let editor: IEditor; let getSelectionSpy: jasmine.Spy; @@ -3255,11 +3256,16 @@ describe('SelectionPlugin on Safari', () => { }, addEventListener: addEventListenerSpy, removeEventListener: removeEventListenerSpy, - getSelection: getSelectionSpy, + defaultView: { + getSelection: getSelectionSpy, + }, }); hasFocusSpy = jasmine.createSpy('hasFocus'); isInShadowEditSpy = jasmine.createSpy('isInShadowEdit'); getDOMSelectionSpy = jasmine.createSpy('getDOMSelection'); + getDOMHelperSpy = jasmine.createSpy('getDOMHelper').and.returnValue({ + getSelectionRange: (): null => null, + }); editor = ({ getDocument: getDocumentSpy, @@ -3270,6 +3276,7 @@ describe('SelectionPlugin on Safari', () => { hasFocus: hasFocusSpy, isInShadowEdit: isInShadowEditSpy, getDOMSelection: getDOMSelectionSpy, + getDOMHelper: getDOMHelperSpy, getColorManager: () => ({ getDarkColor: (color: string) => `${DEFAULT_DARK_COLOR_SUFFIX_COLOR}${color}`, }), @@ -3505,10 +3512,12 @@ describe('SelectionPlugin selectionChange on image selected', () => { let hasFocusSpy: jasmine.Spy; let isInShadowEditSpy: jasmine.Spy; let getDOMSelectionSpy: jasmine.Spy; + let getDOMHelperSpy: jasmine.Spy; let editor: IEditor; let setDOMSelectionSpy: jasmine.Spy; let getRangeAtSpy: jasmine.Spy; let getSelectionSpy: jasmine.Spy; + let mockedRange: Range; beforeEach(() => { disposer = jasmine.createSpy('disposer'); @@ -3516,11 +3525,14 @@ describe('SelectionPlugin selectionChange on image selected', () => { attachDomEvent = jasmine.createSpy('attachDomEvent').and.returnValue(disposer); removeEventListenerSpy = jasmine.createSpy('removeEventListener'); addEventListenerSpy = jasmine.createSpy('addEventListener'); - getRangeAtSpy = jasmine.createSpy('getRangeAt'); + mockedRange = { startContainer: {} } as Range; + getRangeAtSpy = jasmine.createSpy('getRangeAt').and.callFake(() => mockedRange); getSelectionSpy = jasmine.createSpy('getSelection').and.returnValue({ focusNode: { nodeName: 'SPAN', }, + focusOffset: 0, + rangeCount: 1, getRangeAt: getRangeAtSpy, }); getDocumentSpy = jasmine.createSpy('getDocument').and.returnValue({ @@ -3529,12 +3541,17 @@ describe('SelectionPlugin selectionChange on image selected', () => { }, addEventListener: addEventListenerSpy, removeEventListener: removeEventListenerSpy, - getSelection: getSelectionSpy, + defaultView: { + getSelection: getSelectionSpy, + }, }); hasFocusSpy = jasmine.createSpy('hasFocus'); isInShadowEditSpy = jasmine.createSpy('isInShadowEdit'); getDOMSelectionSpy = jasmine.createSpy('getDOMSelection'); setDOMSelectionSpy = jasmine.createSpy('setDOMSelection'); + getDOMHelperSpy = jasmine.createSpy('getDOMHelper').and.returnValue({ + getSelectionRange: () => mockedRange, + }); editor = ({ getDocument: getDocumentSpy, @@ -3545,6 +3562,7 @@ describe('SelectionPlugin selectionChange on image selected', () => { hasFocus: hasFocusSpy, isInShadowEdit: isInShadowEditSpy, getDOMSelection: getDOMSelectionSpy, + getDOMHelper: getDOMHelperSpy, setDOMSelection: setDOMSelectionSpy, } as any) as IEditor; }); diff --git a/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts b/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts index 86da6ac65ad5..fa50096a1284 100644 --- a/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts @@ -2,14 +2,35 @@ import * as getRangesByText from 'roosterjs-content-model-dom/lib/domUtils/getRa import { createDOMHelper } from '../../../lib/editor/core/DOMHelperImpl'; import { DOMHelper } from 'roosterjs-content-model-types'; +/** + * Creates a minimal mock HTMLElement for createDOMHelper. + * The constructor needs: getRootNode(), ownerDocument.defaultView.getSelection(). + * Merges any provided ownerDocument props while ensuring defaultView.getSelection exists. + */ +function createMockDiv(props: Record<string, any>): any { + const { ownerDocument, getRootNode, ...rest } = props || {}; + const { defaultView, ...ownerDocRest } = ownerDocument || {}; + return { + ...rest, + getRootNode: getRootNode || (() => document), + ownerDocument: { + ...ownerDocRest, + defaultView: { + getSelection: (): null => null, + ...defaultView, + }, + }, + }; +} + describe('DOMHelperImpl', () => { describe('isNodeInEditor', () => { it('isNodeInEditor', () => { const mockedResult = 'RESULT' as any; const containsSpy = jasmine.createSpy('contains').and.returnValue(mockedResult); - const mockedDiv = { + const mockedDiv = createMockDiv({ contains: containsSpy, - } as any; + }); const domHelper = createDOMHelper(mockedDiv); const mockedNode = 'NODE' as any; @@ -39,9 +60,9 @@ describe('DOMHelperImpl', () => { it('isNodeInEditor, check root node, excludeRoot=true, do not call contains', () => { const containsSpy = jasmine.createSpy('contains'); - const mockedDiv = { + const mockedDiv = createMockDiv({ contains: containsSpy, - } as any; + }); const domHelper = createDOMHelper(mockedDiv); const result = domHelper.isNodeInEditor(mockedDiv, true); @@ -57,9 +78,9 @@ describe('DOMHelperImpl', () => { const querySelectorAllSpy = jasmine .createSpy('querySelectorAll') .and.returnValue(mockedResult); - const mockedDiv: HTMLElement = { + const mockedDiv = createMockDiv({ querySelectorAll: querySelectorAllSpy, - } as any; + }) as any; const mockedSelector = 'SELECTOR'; const domHelper = createDOMHelper(mockedDiv); @@ -73,9 +94,9 @@ describe('DOMHelperImpl', () => { describe('getTextContent', () => { it('getTextContent', () => { const mockedTextContent = 'TEXT'; - const mockedDiv: HTMLDivElement = { + const mockedDiv = createMockDiv({ textContent: mockedTextContent, - } as any; + }) as any; const domHelper = createDOMHelper(mockedDiv); const result = domHelper.getTextContent(); @@ -86,12 +107,12 @@ describe('DOMHelperImpl', () => { describe('calculateZoomScale', () => { it('calculateZoomScale 1', () => { - const mockedDiv = { + const mockedDiv = createMockDiv({ getBoundingClientRect: () => ({ width: 1, }), offsetWidth: 2, - } as any; + }); const domHelper = createDOMHelper(mockedDiv); const zoomScale = domHelper.calculateZoomScale(); @@ -100,12 +121,12 @@ describe('DOMHelperImpl', () => { }); it('calculateZoomScale 2', () => { - const mockedDiv = { + const mockedDiv = createMockDiv({ getBoundingClientRect: () => ({ width: 1, }), offsetWidth: 0, // Wrong number, should return 1 as fallback - } as any; + }); const domHelper = createDOMHelper(mockedDiv); const zoomScale = domHelper.calculateZoomScale(); @@ -119,9 +140,9 @@ describe('DOMHelperImpl', () => { const mockedAttr = 'ATTR'; const mockedValue = 'VALUE'; const getAttributeSpy = jasmine.createSpy('getAttribute').and.returnValue(mockedValue); - const mockedDiv = { + const mockedDiv = createMockDiv({ getAttribute: getAttributeSpy, - } as any; + }); const domHelper = createDOMHelper(mockedDiv); const result = domHelper.getDomAttribute(mockedAttr); @@ -138,10 +159,10 @@ describe('DOMHelperImpl', () => { const mockedValue = 'VALUE'; const setAttributeSpy = jasmine.createSpy('setAttribute'); const removeAttributeSpy = jasmine.createSpy('removeAttribute'); - const mockedDiv = { + const mockedDiv = createMockDiv({ setAttribute: setAttributeSpy, removeAttribute: removeAttributeSpy, - } as any; + }); const domHelper = createDOMHelper(mockedDiv); domHelper.setDomAttribute(mockedAttr1, mockedValue); @@ -160,9 +181,9 @@ describe('DOMHelperImpl', () => { const styleName: keyof CSSStyleDeclaration = 'backgroundColor'; const styleSpy = jasmine.createSpyObj('style', [styleName]); styleSpy[styleName] = mockedValue; - const mockedDiv = { + const mockedDiv = createMockDiv({ style: styleSpy, - } as any; + }); const domHelper = createDOMHelper(mockedDiv); const result = domHelper.getDomStyle(styleName); @@ -237,12 +258,12 @@ describe('DOMHelperImpl', () => { beforeEach(() => { containsSpy = jasmine.createSpy('contains'); - mockedRoot = { + mockedRoot = createMockDiv({ ownerDocument: { activeElement: mockedElement, }, contains: containsSpy, - } as any; + }) as any; domHelper = createDOMHelper(mockedRoot); }); @@ -279,13 +300,13 @@ describe('DOMHelperImpl', () => { beforeEach(() => { getComputedStyleSpy = jasmine.createSpy('getComputedStyle'); - div = { + div = createMockDiv({ ownerDocument: { defaultView: { getComputedStyle: getComputedStyleSpy, }, }, - } as any; + }) as any; }); it('LTR', () => { @@ -322,14 +343,14 @@ describe('DOMHelperImpl', () => { beforeEach(() => { getComputedStyleSpy = jasmine.createSpy('getComputedStyle'); - div = { + div = createMockDiv({ ownerDocument: { defaultView: { getComputedStyle: getComputedStyleSpy, }, }, clientWidth: 1000, - } as any; + }); }); it('getClientWidth', () => { @@ -357,7 +378,7 @@ describe('DOMHelperImpl', () => { const mockedClone = 'CLONE' as any; const cloneSpy = jasmine.createSpy('cloneSpy').and.returnValue(mockedClone); const importNodeSpy = jasmine.createSpy('importNodeSpy').and.returnValue(mockedClone); - const mockedDiv: HTMLElement = { + const mockedDiv = createMockDiv({ cloneNode: cloneSpy, ownerDocument: { implementation: { @@ -366,7 +387,7 @@ describe('DOMHelperImpl', () => { }), }, }, - } as any; + }) as any; const domHelper = createDOMHelper(mockedDiv); const result = domHelper.getClonedRoot(); @@ -379,7 +400,7 @@ describe('DOMHelperImpl', () => { describe('getContainerFormat', () => { it('getContainerFormat', () => { - const mockedDiv: HTMLDivElement = { + const mockedDiv = createMockDiv({ ownerDocument: { defaultView: { getComputedStyle: () => ({ @@ -398,7 +419,7 @@ describe('DOMHelperImpl', () => { }, style: {}, getAttribute: (): null => null, - } as any; + }) as any; const domHelper = createDOMHelper(mockedDiv); const result = domHelper.getContainerFormat(); @@ -419,7 +440,7 @@ describe('DOMHelperImpl', () => { }); it('getContainerFormat use style color', () => { - const mockedDiv: HTMLDivElement = { + const mockedDiv = createMockDiv({ ownerDocument: { defaultView: { getComputedStyle: () => ({ @@ -441,7 +462,7 @@ describe('DOMHelperImpl', () => { backgroundColor: 'style-bg-color', }, getAttribute: (): null => null, - } as any; + }) as any; const domHelper = createDOMHelper(mockedDiv); const result = domHelper.getContainerFormat(); @@ -462,7 +483,7 @@ describe('DOMHelperImpl', () => { }); it('getContainerFormat use style color in dark mode', () => { - const mockedDiv: HTMLDivElement = { + const mockedDiv = createMockDiv({ ownerDocument: { defaultView: { getComputedStyle: () => ({ @@ -484,7 +505,7 @@ describe('DOMHelperImpl', () => { backgroundColor: 'var(--darkBgColor, lightBgColor)', }, getAttribute: (): null => null, - } as any; + }) as any; const mockDarkHandler = {} as any; const domHelper = createDOMHelper(mockedDiv); @@ -507,7 +528,7 @@ describe('DOMHelperImpl', () => { }); it('getContainerFormat use runtime color in dark mode', () => { - const mockedDiv: HTMLDivElement = { + const mockedDiv = createMockDiv({ ownerDocument: { defaultView: { getComputedStyle: () => ({ @@ -526,7 +547,7 @@ describe('DOMHelperImpl', () => { }, style: {}, getAttribute: (): null => null, - } as any; + }) as any; const mockDarkHandler = {} as any; const domHelper = createDOMHelper(mockedDiv); @@ -650,4 +671,217 @@ describe('DOMHelperImpl', () => { expect(ranges).toBe(getRangesByTextSpy.calls.mostRecent().returnValue); }); }); + + describe('getSelectionRange', () => { + it('returns null when no selection', () => { + const mockedDiv = createMockDiv({ + ownerDocument: { + defaultView: { + getSelection: (): null => null, + }, + }, + }); + const domHelper = createDOMHelper(mockedDiv); + + expect(domHelper.getSelectionRange()).toBeNull(); + }); + + it('returns null when rangeCount is 0', () => { + const mockedDiv = createMockDiv({ + ownerDocument: { + defaultView: { + getSelection: () => ({ rangeCount: 0 }), + }, + }, + }); + const domHelper = createDOMHelper(mockedDiv); + + expect(domHelper.getSelectionRange()).toBeNull(); + }); + + it('returns range when selection has range', () => { + const mockedRange = document.createRange(); + const mockedDiv = createMockDiv({ + ownerDocument: { + defaultView: { + getSelection: () => ({ rangeCount: 1, getRangeAt: () => mockedRange }), + }, + }, + }); + const domHelper = createDOMHelper(mockedDiv); + + expect(domHelper.getSelectionRange()).toBe(mockedRange); + }); + }); + + describe('setSelectionRange', () => { + it('does nothing when no selection object', () => { + const mockedDiv = createMockDiv({ + ownerDocument: { + defaultView: { + getSelection: (): null => null, + }, + }, + }); + const domHelper = createDOMHelper(mockedDiv); + const range = document.createRange(); + + // Should not throw + domHelper.setSelectionRange(range); + }); + + it('does nothing when ranges are same', () => { + const container = document.createElement('div'); + const textNode = document.createTextNode('hello'); + container.appendChild(textNode); + document.body.appendChild(container); + + const existingRange = document.createRange(); + existingRange.setStart(textNode, 0); + existingRange.setEnd(textNode, 3); + + const newRange = document.createRange(); + newRange.setStart(textNode, 0); + newRange.setEnd(textNode, 3); + + const setBaseAndExtentSpy = jasmine.createSpy('setBaseAndExtent'); + const mockedDiv = createMockDiv({ + ownerDocument: { + defaultView: { + getSelection: () => ({ + rangeCount: 1, + getRangeAt: () => existingRange, + setBaseAndExtent: setBaseAndExtentSpy, + }), + }, + createRange: () => document.createRange(), + }, + }); + const domHelper = createDOMHelper(mockedDiv); + + domHelper.setSelectionRange(newRange); + + expect(setBaseAndExtentSpy).not.toHaveBeenCalled(); + document.body.removeChild(container); + }); + + it('sets selection forward when not reverted', () => { + const container = document.createElement('div'); + const textNode = document.createTextNode('hello world'); + container.appendChild(textNode); + document.body.appendChild(container); + + const existingRange = document.createRange(); + existingRange.setStart(textNode, 0); + existingRange.setEnd(textNode, 3); + + const newRange = document.createRange(); + newRange.setStart(textNode, 0); + newRange.setEnd(textNode, 5); + + const setBaseAndExtentSpy = jasmine.createSpy('setBaseAndExtent'); + const mockedDiv = createMockDiv({ + ownerDocument: { + defaultView: { + getSelection: () => ({ + rangeCount: 1, + getRangeAt: () => existingRange, + setBaseAndExtent: setBaseAndExtentSpy, + removeAllRanges: jasmine.createSpy('removeAllRanges'), + }), + }, + createRange: () => document.createRange(), + }, + }); + const domHelper = createDOMHelper(mockedDiv); + + domHelper.setSelectionRange(newRange, false); + + expect(setBaseAndExtentSpy).toHaveBeenCalledWith(textNode, 0, textNode, 5); + document.body.removeChild(container); + }); + + it('sets selection reverted when isReverted is true', () => { + const container = document.createElement('div'); + const textNode = document.createTextNode('hello world'); + container.appendChild(textNode); + document.body.appendChild(container); + + const existingRange = document.createRange(); + existingRange.setStart(textNode, 0); + existingRange.setEnd(textNode, 3); + + const newRange = document.createRange(); + newRange.setStart(textNode, 0); + newRange.setEnd(textNode, 5); + + const setBaseAndExtentSpy = jasmine.createSpy('setBaseAndExtent'); + const mockedDiv = createMockDiv({ + ownerDocument: { + defaultView: { + getSelection: () => ({ + rangeCount: 1, + getRangeAt: () => existingRange, + setBaseAndExtent: setBaseAndExtentSpy, + removeAllRanges: jasmine.createSpy('removeAllRanges'), + }), + }, + createRange: () => document.createRange(), + }, + }); + const domHelper = createDOMHelper(mockedDiv); + + domHelper.setSelectionRange(newRange, true); + + expect(setBaseAndExtentSpy).toHaveBeenCalledWith(textNode, 5, textNode, 0); + document.body.removeChild(container); + }); + }); + + describe('appendToRoot', () => { + it('appends to document.body when no shadow root', () => { + const div = document.createElement('div'); + const domHelper = createDOMHelper(div); + const element = document.createElement('span'); + + domHelper.appendToRoot(element); + + expect(document.body.contains(element)).toBeTrue(); + document.body.removeChild(element); + }); + + it('appends to shadow root when in shadow DOM', () => { + const host = document.createElement('div'); + document.body.appendChild(host); + const shadowRoot = host.attachShadow({ mode: 'open' }); + const contentDiv = document.createElement('div'); + shadowRoot.appendChild(contentDiv); + + const domHelper = createDOMHelper(contentDiv, { useShadowDom: true }); + const element = document.createElement('span'); + + domHelper.appendToRoot(element); + + expect(shadowRoot.contains(element)).toBeTrue(); + document.body.removeChild(host); + }); + }); + + describe('hasFocus in shadow DOM', () => { + it('uses shadowRoot.activeElement when in shadow DOM', () => { + const host = document.createElement('div'); + document.body.appendChild(host); + const shadowRoot = host.attachShadow({ mode: 'open' }); + const contentDiv = document.createElement('div'); + contentDiv.setAttribute('contenteditable', 'true'); + shadowRoot.appendChild(contentDiv); + + const domHelper = createDOMHelper(contentDiv, { useShadowDom: true }); + + // When no element is focused in the shadow root + expect(domHelper.hasFocus()).toBeFalse(); + + document.body.removeChild(host); + }); + }); }); diff --git a/packages/roosterjs-content-model-core/test/overrides/containerSizeFormatParserTest.ts b/packages/roosterjs-content-model-core/test/overrides/containerSizeFormatParserTest.ts index d8a98670fb2c..bc9a1fb6ae92 100644 --- a/packages/roosterjs-content-model-core/test/overrides/containerSizeFormatParserTest.ts +++ b/packages/roosterjs-content-model-core/test/overrides/containerSizeFormatParserTest.ts @@ -58,4 +58,67 @@ describe('containerSizeFormatParser', () => { expect(format).toEqual({}); }); + + it('DIV with maxWidth', () => { + const div = document.createElement('div'); + const format: SizeFormat = { + maxWidth: '100px', + }; + + containerSizeFormatParser(format, div, null!, {}); + + expect(format).toEqual({}); + }); + + it('DIV with maxHeight', () => { + const div = document.createElement('div'); + const format: SizeFormat = { + maxHeight: '100px', + }; + + containerSizeFormatParser(format, div, null!, {}); + + expect(format).toEqual({}); + }); + + it('DIV with all size properties', () => { + const div = document.createElement('div'); + const format: SizeFormat = { + width: '10px', + height: '10px', + maxWidth: '100px', + maxHeight: '100px', + }; + + containerSizeFormatParser(format, div, null!, {}); + + expect(format).toEqual({}); + }); + + it('P with maxWidth and maxHeight', () => { + const p = document.createElement('p'); + const format: SizeFormat = { + maxWidth: '200px', + maxHeight: '50px', + }; + + containerSizeFormatParser(format, p, null!, {}); + + expect(format).toEqual({}); + }); + + it('SPAN with maxWidth and maxHeight', () => { + const span = document.createElement('span'); + const format: SizeFormat = { + maxWidth: '200px', + maxHeight: '50px', + }; + + containerSizeFormatParser(format, span, null!, {}); + + expect(format).toEqual({ + maxWidth: '200px', + maxHeight: '50px', + }); + }); }); diff --git a/packages/roosterjs-content-model-core/test/testUtils/createMockDomHelper.ts b/packages/roosterjs-content-model-core/test/testUtils/createMockDomHelper.ts new file mode 100644 index 000000000000..fe1c2d08fb42 --- /dev/null +++ b/packages/roosterjs-content-model-core/test/testUtils/createMockDomHelper.ts @@ -0,0 +1,36 @@ +import type { DOMHelper } from 'roosterjs-content-model-types'; + +/** + * Creates a mock DOMHelper with safe no-op defaults. Pass spies or custom implementations + * via `overrides` to control specific methods in your test. + */ +export function createMockDomHelper( + overrides?: Partial<DOMHelper> +): DOMHelper & { [K in keyof DOMHelper]: jasmine.Spy } { + const defaults: DOMHelper = { + isNodeInEditor: jasmine.createSpy('isNodeInEditor').and.returnValue(false), + queryElements: jasmine.createSpy('queryElements').and.returnValue([]), + getTextContent: jasmine.createSpy('getTextContent').and.returnValue(''), + calculateZoomScale: jasmine.createSpy('calculateZoomScale').and.returnValue(1), + setDomAttribute: jasmine.createSpy('setDomAttribute'), + getDomAttribute: jasmine.createSpy('getDomAttribute').and.returnValue(null), + getDomStyle: jasmine.createSpy('getDomStyle').and.returnValue(''), + findClosestElementAncestor: jasmine + .createSpy('findClosestElementAncestor') + .and.returnValue(null), + findClosestBlockElement: jasmine + .createSpy('findClosestBlockElement') + .and.returnValue(null as any), + hasFocus: jasmine.createSpy('hasFocus').and.returnValue(false), + isRightToLeft: jasmine.createSpy('isRightToLeft').and.returnValue(false), + getClientWidth: jasmine.createSpy('getClientWidth').and.returnValue(800), + getClonedRoot: jasmine.createSpy('getClonedRoot').and.returnValue(null as any), + getContainerFormat: jasmine.createSpy('getContainerFormat').and.returnValue({}), + getRangesByText: jasmine.createSpy('getRangesByText').and.returnValue([]), + getSelectionRange: jasmine.createSpy('getSelectionRange').and.returnValue(null), + setSelectionRange: jasmine.createSpy('setSelectionRange'), + appendToRoot: jasmine.createSpy('appendToRoot'), + }; + + return { ...defaults, ...overrides } as any; +} diff --git a/packages/roosterjs-content-model-dom/lib/formatHandlers/block/directionFormatHandler.ts b/packages/roosterjs-content-model-dom/lib/formatHandlers/block/directionFormatHandler.ts index fdcecdd2bfec..55603d3ae997 100644 --- a/packages/roosterjs-content-model-dom/lib/formatHandlers/block/directionFormatHandler.ts +++ b/packages/roosterjs-content-model-dom/lib/formatHandlers/block/directionFormatHandler.ts @@ -13,12 +13,16 @@ export const directionFormatHandler: FormatHandler<DirectionFormat> = { format.direction = dir == 'rtl' ? 'rtl' : 'ltr'; } }, - apply: (format, element) => { + apply: (format, element, context) => { if (format.direction) { element.style.direction = format.direction; } - if (format.direction == 'rtl' && isElementOfType(element, 'table')) { + if ( + format.direction == 'rtl' && + isElementOfType(element, 'table') && + context.implicitFormat.direction != 'rtl' + ) { element.style.justifySelf = 'flex-end'; } }, diff --git a/packages/roosterjs-content-model-dom/lib/formatHandlers/common/borderFormatHandler.ts b/packages/roosterjs-content-model-dom/lib/formatHandlers/common/borderFormatHandler.ts index ca45787a6943..f46ad497bdbd 100644 --- a/packages/roosterjs-content-model-dom/lib/formatHandlers/common/borderFormatHandler.ts +++ b/packages/roosterjs-content-model-dom/lib/formatHandlers/common/borderFormatHandler.ts @@ -1,5 +1,6 @@ import { BorderKeys } from '../utils/borderKeys'; import type { BorderFormat } from 'roosterjs-content-model-types'; +import { combineBorderValue, extractBorderValues } from '../../domUtils/style/borderValues'; import type { FormatHandler } from '../FormatHandler'; // This array needs to match BorderKeys array @@ -34,7 +35,17 @@ export const borderFormatHandler: FormatHandler<BorderFormat> = { } if (value && width != defaultWidth) { - format[key] = value == 'none' ? '' : value; + let result = value; + if (result.includes('initial')) { + // Remove 'initial' from the last part (color) of the border value + // since browsers ignore it when setting the inline style property + const border = extractBorderValues(value); + if (border.color === 'initial') { + border.color = ''; + } + result = combineBorderValue(border); + } + format[key] = result == 'none' ? '' : result; } }); diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/addTextSegment.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/addTextSegment.ts index dc8536468de5..4a93d275148c 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/addTextSegment.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/addTextSegment.ts @@ -4,6 +4,7 @@ import { createText } from '../creators/createText'; import { ensureParagraph } from './ensureParagraph'; import { hasSpacesOnly } from './hasSpacesOnly'; import { isWhiteSpacePreserved } from '../../domUtils/isWhiteSpacePreserved'; +import { stripInvisibleUnicode } from './stripInvisibleUnicode'; import type { ContentModelBlockGroup, ContentModelText, @@ -32,7 +33,13 @@ export function addTextSegment( (paragraph?.segments.length ?? 0) > 0 || isWhiteSpacePreserved(paragraph?.format.whiteSpace) ) { - textModel = createText(text, context.segmentFormat); + const filteredText = + context.experimentalFeatures && + context.experimentalFeatures.indexOf('FilterInvisibleUnicode') > -1 + ? stripInvisibleUnicode(text) + : text; + + textModel = createText(filteredText, context.segmentFormat); if (context.isInSelection) { textModel.isSelected = true; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/stripInvisibleUnicode.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/stripInvisibleUnicode.ts new file mode 100644 index 000000000000..07e9858d369a --- /dev/null +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/stripInvisibleUnicode.ts @@ -0,0 +1,14 @@ +// According to https://embracethered.com/blog/posts/2024/hiding-and-finding-text-with-unicode-tags/ +// there are some invisible unicode characters in the range of U+E0000 to U+EFFFF, which are used for hiding text in HTML. +// We need to strip them out before processing the pasted content, otherwise they will be treated as normal text and cause unexpected behavior. +const INVISIBLE_UNICODE_REGEX = /[\u{E0000}-\u{EFFFF}]/gu; + +/** + * @internal + * Strip invisible unicode characters from the given string + * @param value The string to be processed + * @returns The string with invisible unicode characters removed + */ +export function stripInvisibleUnicode(value: string): string { + return value.replace(INVISIBLE_UNICODE_REGEX, ''); +} diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createSelectionMarker.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createSelectionMarker.ts index e8ec164d896b..19116335ffc9 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createSelectionMarker.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createSelectionMarker.ts @@ -1,3 +1,5 @@ +import { EmptySegmentFormat } from '../../constants/EmptySegmentFormat'; +import { getObjectKeys } from '../../domUtils/getObjectKeys'; import type { ContentModelSegmentFormat, ContentModelSelectionMarker, @@ -10,9 +12,19 @@ import type { export function createSelectionMarker( format?: Readonly<ContentModelSegmentFormat> ): ContentModelSelectionMarker { + const filteredFormat: ContentModelSegmentFormat = {}; + + if (format) { + getObjectKeys(EmptySegmentFormat).forEach(key => { + if (key in format) { + (filteredFormat[key] as any) = format[key]; + } + }); + } + return { segmentType: 'SelectionMarker', isSelected: true, - format: { ...format }, + format: filteredFormat, }; } diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleFormatContainer.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleFormatContainer.ts index da761c33f25d..d6895af24458 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleFormatContainer.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleFormatContainer.ts @@ -53,13 +53,29 @@ export const handleFormatContainer: ContentModelBlockHandler<ContentModelFormatC applyFormat(containerNode, context.formatAppliers.container, container.format, context); }); - if (container.tagName == 'pre') { - stackFormat(context, PreChildFormat, () => { - context.modelHandlers.blockGroupChildren(doc, containerNode, container, context); - }); - } else { - context.modelHandlers.blockGroupChildren(doc, containerNode, container, context); - } + stackFormat( + context, + container.format.direction ? { direction: container.format.direction } : null, + () => { + if (container.tagName == 'pre') { + stackFormat(context, PreChildFormat, () => { + context.modelHandlers.blockGroupChildren( + doc, + containerNode, + container, + context + ); + }); + } else { + context.modelHandlers.blockGroupChildren( + doc, + containerNode, + container, + context + ); + } + } + ); element = containerNode; } diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleListItem.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleListItem.ts index 1d6d51e42f43..16e4901d7212 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleListItem.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleListItem.ts @@ -76,7 +76,13 @@ export const handleListItem: ContentModelBlockHandler<ContentModelListItem> = ( applyFormat(li, context.formatAppliers.listItemElement, listItem.format, context); stackFormat(context, listItem.formatHolder.format, () => { - context.modelHandlers.blockGroupChildren(doc, li, listItem, context); + stackFormat( + context, + listItem.format.direction ? { direction: listItem.format.direction } : null, + () => { + context.modelHandlers.blockGroupChildren(doc, li, listItem, context); + } + ); }); } else { // There is no level for this list item, that means it should be moved out of the list diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleTable.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleTable.ts index 2f183cf580b2..57f5c24f434c 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleTable.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleTable.ts @@ -150,7 +150,13 @@ export const handleTable: ContentModelBlockHandler<ContentModelTable> = ( applyFormat(td, context.formatAppliers.dataset, cell.dataset, context); } - context.modelHandlers.blockGroupChildren(doc, td, cell, context); + stackFormat( + context, + cell.format.direction ? { direction: cell.format.direction } : null, + () => { + context.modelHandlers.blockGroupChildren(doc, td, cell, context); + } + ); }); context.onNodeCreated?.(cell, td); diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts index 00b1eba1501a..db1d79f568c8 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts @@ -125,8 +125,8 @@ describe('childProcessor', () => { }; context.pendingFormat = { format: { - a: 'a', - } as any, + fontFamily: 'Arial', + }, insertPoint: { node: div, offset: 0, @@ -144,12 +144,12 @@ describe('childProcessor', () => { segments: [ { segmentType: 'SelectionMarker', - format: { a: 'a' } as any, + format: { fontFamily: 'Arial' }, isSelected: true, }, ], isImplicit: true, - segmentFormat: { a: 'a' } as any, + segmentFormat: { fontFamily: 'Arial' }, }, ], }); @@ -404,7 +404,7 @@ describe('childProcessor', () => { it('Process with segment format and selection marker', () => { const div = document.createElement('div'); - context.segmentFormat = { a: 'b' } as any; + context.segmentFormat = { fontFamily: 'Arial' }; context.selection = { type: 'range', range: { @@ -423,10 +423,14 @@ describe('childProcessor', () => { expect(doc.blocks[0]).toEqual({ blockType: 'Paragraph', segments: [ - { segmentType: 'SelectionMarker', format: { a: 'b' } as any, isSelected: true }, + { + segmentType: 'SelectionMarker', + format: { fontFamily: 'Arial' }, + isSelected: true, + }, ], isImplicit: true, - segmentFormat: { a: 'b' } as any, + segmentFormat: { fontFamily: 'Arial' }, format: {}, }); }); diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts index 7c11c4263c36..d7f726499922 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts @@ -883,8 +883,8 @@ describe('textProcessor', () => { }; context.pendingFormat = { format: { - a: 'a', - } as any, + fontFamily: 'Arial', + }, insertPoint: { node: text, offset: 2, @@ -907,7 +907,7 @@ describe('textProcessor', () => { }, { segmentType: 'SelectionMarker', - format: { a: 'a' } as any, + format: { fontFamily: 'Arial' }, isSelected: true, }, { diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/textWithSelectionProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/textWithSelectionProcessorTest.ts index 104c19654a07..e40293bac43a 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/textWithSelectionProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/textWithSelectionProcessorTest.ts @@ -574,8 +574,8 @@ describe('textWithSelectionProcessor', () => { }; context.pendingFormat = { format: { - a: 'a', - } as any, + fontFamily: 'Arial', + }, insertPoint: { node: text, offset: 2, @@ -598,7 +598,7 @@ describe('textWithSelectionProcessor', () => { }, { segmentType: 'SelectionMarker', - format: { a: 'a' } as any, + format: { fontFamily: 'Arial' }, isSelected: true, }, { diff --git a/packages/roosterjs-content-model-dom/test/domToModel/utils/addSelectionMarkerTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/utils/addSelectionMarkerTest.ts index fb7f401f363f..9d554eebf94f 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/utils/addSelectionMarkerTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/utils/addSelectionMarkerTest.ts @@ -209,15 +209,15 @@ describe('addSelectionMarker', () => { const doc = createContentModelDocument(); const context = createDomToModelContext({ defaultFormat: { - a: 'a', - b: 'b', - c: 'c', - } as any, + fontFamily: 'Arial', + fontSize: '10px', + fontWeight: 'normal', + }, pendingFormat: { format: { - c: 'c3', - e: 'e', - } as any, + fontWeight: 'bold', + lineHeight: '1.5', + }, insertPoint: { node: mockedContainer, offset: mockedOffset, @@ -226,10 +226,10 @@ describe('addSelectionMarker', () => { }); context.segmentFormat = { - b: 'b2', - c: 'c2', - d: 'd', - } as any; + fontSize: '12px', + fontWeight: '500', + letterSpacing: '1px', + }; doc.blocks.push(createParagraph()); addSelectionMarker(doc, context, mockedContainer, mockedOffset); @@ -245,12 +245,12 @@ describe('addSelectionMarker', () => { segmentType: 'SelectionMarker', isSelected: true, format: { - a: 'a', - b: 'b2', - c: 'c3', - d: 'd', - e: 'e', - } as any, + fontFamily: 'Arial', + fontSize: '12px', + fontWeight: 'bold', + letterSpacing: '1px', + lineHeight: '1.5', + }, }, ], }, diff --git a/packages/roosterjs-content-model-dom/test/endToEndTest.ts b/packages/roosterjs-content-model-dom/test/endToEndTest.ts index 8d55177d6929..87807d48cbdc 100644 --- a/packages/roosterjs-content-model-dom/test/endToEndTest.ts +++ b/packages/roosterjs-content-model-dom/test/endToEndTest.ts @@ -567,6 +567,249 @@ describe('End to end test for DOM => Model => DOM/TEXT', () => { ); }); + it('LTR table under RTL table', () => { + runTest( + '<table dir="rtl"><tr><td><table dir="ltr"><tr><td>bb</td></tr></table></td></tr></table>', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + format: {}, + height: 0, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Table', + rows: [ + { + format: {}, + height: 0, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'bb', + format: {}, + }, + ], + format: { + direction: 'ltr', + }, + isImplicit: true, + }, + ], + format: { + direction: 'ltr', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { + direction: 'ltr', + }, + widths: [], + dataset: {}, + }, + ], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { direction: 'rtl' }, + widths: [], + dataset: {}, + }, + ], + }, + 'bb', + '<table style="direction: rtl; justify-self: flex-end;"><tbody><tr><td style="direction: rtl;"><table style="direction: ltr;"><tbody><tr><td style="direction: ltr;"><div style="direction: ltr;">bb</div></td></tr></tbody></table></td></tr></tbody></table>' + ); + }); + + it('RTL table under LTR table', () => { + runTest( + '<table dir="ltr"><tr><td><table dir="rtl"><tr><td>bb</td></tr></table></td></tr></table>', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + format: {}, + height: 0, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Table', + rows: [ + { + format: {}, + height: 0, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'bb', + format: {}, + }, + ], + format: { + direction: 'rtl', + }, + isImplicit: true, + }, + ], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { + direction: 'rtl', + }, + widths: [], + dataset: {}, + }, + ], + format: { + direction: 'ltr', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { direction: 'ltr' }, + widths: [], + dataset: {}, + }, + ], + }, + 'bb', + '<table style="direction: ltr;"><tbody><tr><td style="direction: ltr;"><table style="direction: rtl; justify-self: flex-end;"><tbody><tr><td style="direction: rtl;"><div style="direction: rtl;">bb</div></td></tr></tbody></table></td></tr></tbody></table>' + ); + }); + + it('RTL table under RTL table', () => { + runTest( + '<table dir="rtl"><tr><td><table dir="rtl"><tr><td>bb</td></tr></table></td></tr></table>', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + format: {}, + height: 0, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Table', + rows: [ + { + format: {}, + height: 0, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'bb', + format: {}, + }, + ], + format: { + direction: 'rtl', + }, + isImplicit: true, + }, + ], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { + direction: 'rtl', + }, + widths: [], + dataset: {}, + }, + ], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { direction: 'rtl' }, + widths: [], + dataset: {}, + }, + ], + }, + 'bb', + '<table style="direction: rtl; justify-self: flex-end;"><tbody><tr><td style="direction: rtl;"><table style="direction: rtl;"><tbody><tr><td style="direction: rtl;"><div style="direction: rtl;">bb</div></td></tr></tbody></table></td></tr></tbody></table>' + ); + }); + it('Table under styled block', () => { runTest( '<b style="background-color:red; display: block">aa<table><tr><td>bb</td></tr></table>cc</b>', @@ -3028,6 +3271,77 @@ describe('End to end test for DOM => Model => DOM/TEXT', () => { ); }); + it('Text with invisible unicode tag characters is stripped when FilterInvisibleUnicode feature is enabled', () => { + // Source HTML contains U+E0041 / U+E0042 (unicode tag range — must be stripped) + // mixed with U+200B (ZWSP), U+200D (ZWJ), U+202E (RLO), U+202C (PDF) + // which must be preserved. + const div1 = document.createElement('div'); + div1.innerHTML = '<p>a\u{E0041}b\u{200B}c\u{E0042}d\u{202E}evil\u{202C}e</p>'; + + const model = domToContentModel( + div1, + createDomToModelContext({ experimentalFeatures: ['FilterInvisibleUnicode'] }) + ); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'ab\u{200B}cd\u{202E}evil\u{202C}e', + format: {}, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + ], + }); + + const text = contentModelToText(model); + expect(text).toBe('ab\u{200B}cd\u{202E}evil\u{202C}e'); + }); + + it('Text with invisible unicode tag characters is NOT stripped when feature is disabled', () => { + const div1 = document.createElement('div'); + div1.innerHTML = '<p>a\u{E0041}b\u{E0042}c</p>'; + + const model = domToContentModel(div1, createDomToModelContext()); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'a\u{E0041}b\u{E0042}c', + format: {}, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + ], + }); + }); + it('LI without UL followed by other blocks', () => { runTest( '<li>test</li><div>other</div>', diff --git a/packages/roosterjs-content-model-dom/test/formatHandlers/block/directionFormatHandlerTest.ts b/packages/roosterjs-content-model-dom/test/formatHandlers/block/directionFormatHandlerTest.ts index 23f1e9d03af0..ab1b96b19c95 100644 --- a/packages/roosterjs-content-model-dom/test/formatHandlers/block/directionFormatHandlerTest.ts +++ b/packages/roosterjs-content-model-dom/test/formatHandlers/block/directionFormatHandlerTest.ts @@ -90,4 +90,29 @@ describe('directionFormatHandler.apply', () => { '<table style="direction: rtl; justify-self: flex-end;"></table>' ); }); + + it('RTL on table, parent implicit direction is LTR, applies justify-self', () => { + const table = document.createElement('table'); + format.direction = 'rtl'; + context.implicitFormat.direction = 'ltr'; + directionFormatHandler.apply(format, table, context); + expect(table.outerHTML).toBe( + '<table style="direction: rtl; justify-self: flex-end;"></table>' + ); + }); + + it('RTL on table, parent implicit direction is RTL, skips justify-self', () => { + const table = document.createElement('table'); + format.direction = 'rtl'; + context.implicitFormat.direction = 'rtl'; + directionFormatHandler.apply(format, table, context); + expect(table.outerHTML).toBe('<table style="direction: rtl;"></table>'); + }); + + it('RTL on non-table element, never applies justify-self', () => { + const td = document.createElement('td'); + format.direction = 'rtl'; + directionFormatHandler.apply(format, td, context); + expect(td.outerHTML).toBe('<td style="direction: rtl;"></td>'); + }); }); diff --git a/packages/roosterjs-content-model-dom/test/formatHandlers/common/borderFormatHandlerTest.ts b/packages/roosterjs-content-model-dom/test/formatHandlers/common/borderFormatHandlerTest.ts index d989ab9ff5f7..12437b9da503 100644 --- a/packages/roosterjs-content-model-dom/test/formatHandlers/common/borderFormatHandlerTest.ts +++ b/packages/roosterjs-content-model-dom/test/formatHandlers/common/borderFormatHandlerTest.ts @@ -58,10 +58,10 @@ describe('borderFormatHandler.parse', () => { borderFormatHandler.parse(format, div, context, {}); expect(format).toEqual({ - borderTop: jasmine.stringMatching(/1px (none )?red/), - borderRight: jasmine.stringMatching(/2px (none )?red/), - borderBottom: jasmine.stringMatching(/3px (none )?red/), - borderLeft: jasmine.stringMatching(/4px (none )?red/), + borderTop: jasmine.stringMatching(/1px\s+(none\s+)?red/), + borderRight: jasmine.stringMatching(/2px\s+(none\s+)?red/), + borderBottom: jasmine.stringMatching(/3px\s+(none\s+)?red/), + borderLeft: jasmine.stringMatching(/4px\s+(none\s+)?red/), }); }); @@ -73,6 +73,36 @@ describe('borderFormatHandler.parse', () => { expect(format).toEqual({}); }); + it('Has multi-value border-style shorthand', () => { + div.style.borderStyle = 'solid dotted double dashed'; + div.style.borderWidth = '1px'; + div.style.borderColor = 'red'; + + borderFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + borderTop: '1px solid red', + borderRight: '1px dotted red', + borderBottom: '1px double red', + borderLeft: '1px dashed red', + }); + }); + + it('Has multi-value border-style shorthand with 3 values', () => { + div.style.borderStyle = 'solid dotted double'; + div.style.borderWidth = '1px'; + div.style.borderColor = 'red'; + + borderFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + borderTop: '1px solid red', + borderRight: '1px dotted red', + borderBottom: '1px double red', + borderLeft: '1px dotted red', + }); + }); + it('Has 0 width border', () => { div.style.border = '0px sold black'; @@ -143,6 +173,35 @@ describe('borderFormatHandler.parse', () => { borderBottomRightRadius: '10px', }); }); + + it('Should strip initial color from border value', () => { + const mockElement = ({ + style: { + borderTop: '1px solid initial', + borderRight: '1px solid initial', + borderBottom: '1px solid initial', + borderLeft: '1px solid initial', + borderTopWidth: '1px', + borderRightWidth: '1px', + borderBottomWidth: '1px', + borderLeftWidth: '1px', + borderRadius: '', + borderTopLeftRadius: '', + borderTopRightRadius: '', + borderBottomLeftRadius: '', + borderBottomRightRadius: '', + }, + } as unknown) as HTMLElement; + + borderFormatHandler.parse(format, mockElement, context, {}); + + expect(format).toEqual({ + borderTop: '1px solid', + borderRight: '1px solid', + borderBottom: '1px solid', + borderLeft: '1px solid', + }); + }); }); describe('borderFormatHandler.apply', () => { diff --git a/packages/roosterjs-content-model-dom/test/modelApi/common/addTextSegmentTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/common/addTextSegmentTest.ts index c3ccabd1dfa1..9d83c04ca885 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/common/addTextSegmentTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/common/addTextSegmentTest.ts @@ -206,4 +206,56 @@ describe('addTextSegment', () => { ], }); }); + + it('Add text with invisible unicode, feature enabled', () => { + const group = createContentModelDocument(); + const context = createDomToModelContext({ + experimentalFeatures: ['FilterInvisibleUnicode'], + }); + + addTextSegment(group, 'a\u{E0041}b\u{E0042}c', context); + + expect(group).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'abc', + format: {}, + }, + ], + isImplicit: true, + }, + ], + }); + }); + + it('Add text with invisible unicode, feature disabled', () => { + const group = createContentModelDocument(); + const context = createDomToModelContext(); + + addTextSegment(group, 'a\u{E0041}b\u{E0042}c', context); + + expect(group).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'a\u{E0041}b\u{E0042}c', + format: {}, + }, + ], + isImplicit: true, + }, + ], + }); + }); }); diff --git a/packages/roosterjs-content-model-dom/test/modelApi/common/stripInvisibleUnicodeTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/common/stripInvisibleUnicodeTest.ts new file mode 100644 index 000000000000..bae02f32005d --- /dev/null +++ b/packages/roosterjs-content-model-dom/test/modelApi/common/stripInvisibleUnicodeTest.ts @@ -0,0 +1,46 @@ +import { stripInvisibleUnicode } from '../../../lib/modelApi/common/stripInvisibleUnicode'; + +describe('stripInvisibleUnicode', () => { + it('should strip invisible unicode characters in the tag range', () => { + expect(stripInvisibleUnicode('a\u{E0041}b\u{E0042}c')).toBe('abc'); + }); + + it('should strip all characters when input contains only invisible unicode', () => { + expect(stripInvisibleUnicode('\u{E0000}\u{E007F}\u{EFFFF}')).toBe(''); + }); + + it('should strip characters at range boundaries (U+E0000 and U+EFFFF)', () => { + expect(stripInvisibleUnicode('\u{DFFFF}start\u{E0000}mid\u{EFFFF}end\u{F0000}')).toBe( + '\u{DFFFF}startmidend\u{F0000}' + ); + }); + + it('should preserve meaningful invisible characters outside the tag range', () => { + // U+200B = Zero-Width Space, U+200D = Zero-Width Joiner, + // U+202E = Right-to-Left Override, U+202C = Pop Directional Formatting + const text = 'a\u{200B}b\u{200D}c\u{202E}d\u{202C}e'; + expect(stripInvisibleUnicode(text)).toBe(text); + }); + + it('should strip tag-range chars while keeping meaningful invisible chars', () => { + expect(stripInvisibleUnicode('a\u{200B}\u{E0041}b\u{202E}\u{E0042}c')).toBe( + 'a\u{200B}b\u{202E}c' + ); + }); + + it('should not modify visible characters', () => { + const text = 'hello world 你好'; + expect(stripInvisibleUnicode(text)).toBe(text); + }); + + it('should return empty string for empty input', () => { + expect(stripInvisibleUnicode('')).toBe(''); + }); + + it('should handle a long sequence of tag characters', () => { + const tags = Array.from({ length: 100 }, (_, i) => String.fromCodePoint(0xe0000 + i)).join( + '' + ); + expect(stripInvisibleUnicode('before' + tags + 'after')).toBe('beforeafter'); + }); +}); diff --git a/packages/roosterjs-content-model-dom/test/modelApi/creators/creatorsTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/creators/creatorsTest.ts index c0441215e0e3..21dae0216a4e 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/creators/creatorsTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/creators/creatorsTest.ts @@ -233,6 +233,63 @@ describe('Creators', () => { }); }); + it('createText with invisible unicode characters does not strip by default', () => { + const text = 'a\u{E0041}b\u{E0042}c'; + const result = createText(text); + + expect(result).toEqual({ + segmentType: 'Text', + format: {}, + text: 'a\u{E0041}b\u{E0042}c', + }); + }); + + it('createText with only invisible unicode characters does not strip by default', () => { + const text = '\u{E0000}\u{E007F}\u{EFFFF}'; + const result = createText(text); + + expect(result).toEqual({ + segmentType: 'Text', + format: {}, + text: '\u{E0000}\u{E007F}\u{EFFFF}', + }); + }); + + it('createText with invisible unicode at boundary range does not strip by default', () => { + const text = '\u{DFFFF}start\u{E0000}mid\u{EFFFF}end\u{F0000}'; + const result = createText(text); + + expect(result).toEqual({ + segmentType: 'Text', + format: {}, + text: '\u{DFFFF}start\u{E0000}mid\u{EFFFF}end\u{F0000}', + }); + }); + + it('createText preserves meaningful invisible characters outside the tag range', () => { + // ​ = Zero-Width Space, ‍ = Zero-Width Joiner, + // ‮ = Right-to-Left Override, ‬ = Pop Directional Formatting + const text = 'a​b‍c‮d‬e'; + const result = createText(text); + + expect(result).toEqual({ + segmentType: 'Text', + format: {}, + text: 'a​b‍c‮d‬e', + }); + }); + + it('createText does not strip visible characters', () => { + const text = 'hello world 你好 ​'; + const result = createText(text); + + expect(result).toEqual({ + segmentType: 'Text', + format: {}, + text: 'hello world 你好 ​', + }); + }); + it('createTableRow', () => { const row = createTableRow(); @@ -403,18 +460,18 @@ describe('Creators', () => { }); it('createSelectionMarker with selection', () => { - const format = { a: 1 } as any; + const format = { fontSize: '10px', a: 1 } as any; const marker = createSelectionMarker(format); expect(marker).toEqual({ segmentType: 'SelectionMarker', isSelected: true, - format: { a: 1 } as any, + format: { fontSize: '10px' }, }); - (<any>marker.format).a = 2; + (<any>marker.format).fontSize = '20px'; - expect(format).toEqual({ a: 1 }); + expect(format).toEqual({ fontSize: '10px', a: 1 } as any); }); it('createBr', () => { diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleFormatContainerTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleFormatContainerTest.ts index d03a267af130..48ce9128e70e 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleFormatContainerTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleFormatContainerTest.ts @@ -110,6 +110,63 @@ describe('handleFormatContainer', () => { }); }); + it('RTL container propagates direction into implicitFormat for children', () => { + const parent = document.createElement('div'); + const container = createFormatContainer('div', { direction: 'rtl' }); + const paragraph = createParagraph(); + container.blocks.push(paragraph); + + let capturedDirection: string | undefined; + handleBlockGroupChildren.and.callFake((_doc, _node, _group, ctx) => { + capturedDirection = ctx.implicitFormat.direction; + }); + + handleFormatContainer(document, parent, container, context, null); + + expect(capturedDirection).toBe('rtl'); + // implicitFormat must be restored after stackFormat + expect(context.implicitFormat.direction).toBeUndefined(); + }); + + it('Container without direction does not change implicitFormat for children', () => { + const parent = document.createElement('div'); + const container = createFormatContainer('div'); + const paragraph = createParagraph(); + container.blocks.push(paragraph); + + let capturedDirection: string | undefined; + context.implicitFormat.direction = 'rtl'; + handleBlockGroupChildren.and.callFake((_doc, _node, _group, ctx) => { + capturedDirection = ctx.implicitFormat.direction; + }); + + handleFormatContainer(document, parent, container, context, null); + + expect(capturedDirection).toBe('rtl'); + }); + + it('Pre container with RTL direction propagates both pre format and direction', () => { + const parent = document.createElement('div'); + const container = createFormatContainer('pre', { direction: 'rtl' }); + const paragraph = createParagraph(); + paragraph.segments.push(createText('test')); + container.blocks.push(paragraph); + + let capturedDirection: string | undefined; + let capturedWhiteSpace: string | undefined; + handleBlockGroupChildren.and.callFake((_doc, _node, _group, ctx) => { + capturedDirection = ctx.implicitFormat.direction; + capturedWhiteSpace = ctx.implicitFormat.whiteSpace; + }); + + handleFormatContainer(document, parent, container, context, null); + + expect(capturedDirection).toBe('rtl'); + expect(capturedWhiteSpace).toBe('pre'); + expect(context.implicitFormat.direction).toBeUndefined(); + expect(context.implicitFormat.whiteSpace).toBeUndefined(); + }); + it('With onNodeCreated', () => { const parent = document.createElement('div'); const quote = createFormatContainer('blockquote'); diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemTest.ts index 1d31b5764bf2..324e6b915c07 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemTest.ts @@ -436,6 +436,40 @@ describe('handleListItem without format handler', () => { '<ol start="1"><li><div role="presentation">test1</div><div role="presentation">test2</div><div role="presentation">test3</div><table><tbody><tr><td></td><td></td></tr></tbody></table><div role="presentation">test4</div></li></ol>' ); }); + + it('List item with RTL direction propagates direction into implicitFormat for children', () => { + const parent = document.createElement('div'); + const listItem = createListItem([createListLevel('OL')]); + listItem.format.direction = 'rtl'; + listItem.blocks.push(createParagraph()); + + let capturedDirection: string | undefined; + handleBlockGroupChildrenSpy.and.callFake((_doc, _node, _group, ctx) => { + capturedDirection = ctx.implicitFormat.direction; + }); + + handleListItem(document, parent, listItem, context, null); + + expect(capturedDirection).toBe('rtl'); + // implicitFormat must be restored after stackFormat + expect(context.implicitFormat.direction).toBeUndefined(); + }); + + it('List item without direction does not change implicitFormat for children', () => { + const parent = document.createElement('div'); + const listItem = createListItem([createListLevel('OL')]); + listItem.blocks.push(createParagraph()); + + let capturedDirection: string | undefined; + context.implicitFormat.direction = 'rtl'; + handleBlockGroupChildrenSpy.and.callFake((_doc, _node, _group, ctx) => { + capturedDirection = ctx.implicitFormat.direction; + }); + + handleListItem(document, parent, listItem, context, null); + + expect(capturedDirection).toBe('rtl'); + }); }); describe('handleListItem with cache', () => { diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts index dd01a58e61f7..bb89ee995695 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts @@ -660,6 +660,72 @@ describe('handleTable', () => { ); }); + it('Cell with RTL direction propagates direction into implicitFormat for children', () => { + const parent = document.createElement('div'); + const table = createTable(1); + const cell = createTableCell(false, false, false, { direction: 'rtl' }); + table.rows[0].cells.push(cell); + + let capturedDirection: string | undefined; + context.modelHandlers.blockGroupChildren = jasmine + .createSpy('blockGroupChildren') + .and.callFake( + (_doc: Document, _node: Node, _group: unknown, ctx: ModelToDomContext) => { + capturedDirection = ctx.implicitFormat.direction; + } + ); + + handleTable(document, parent, table, context, null); + + expect(capturedDirection).toBe('rtl'); + // implicitFormat must be restored after stackFormat + expect(context.implicitFormat.direction).toBeUndefined(); + }); + + it('Cell with LTR direction propagates direction into implicitFormat for children', () => { + const parent = document.createElement('div'); + const table = createTable(1); + const cell = createTableCell(false, false, false, { direction: 'ltr' }); + table.rows[0].cells.push(cell); + + let capturedDirection: string | undefined; + context.implicitFormat.direction = 'rtl'; + context.modelHandlers.blockGroupChildren = jasmine + .createSpy('blockGroupChildren') + .and.callFake( + (_doc: Document, _node: Node, _group: unknown, ctx: ModelToDomContext) => { + capturedDirection = ctx.implicitFormat.direction; + } + ); + + handleTable(document, parent, table, context, null); + + expect(capturedDirection).toBe('ltr'); + // implicitFormat must be restored + expect(context.implicitFormat.direction).toBe('rtl'); + }); + + it('Cell without direction does not change implicitFormat for children', () => { + const parent = document.createElement('div'); + const table = createTable(1); + const cell = createTableCell(); + table.rows[0].cells.push(cell); + + let capturedDirection: string | undefined; + context.implicitFormat.direction = 'rtl'; + context.modelHandlers.blockGroupChildren = jasmine + .createSpy('blockGroupChildren') + .and.callFake( + (_doc: Document, _node: Node, _group: unknown, ctx: ModelToDomContext) => { + capturedDirection = ctx.implicitFormat.direction; + } + ); + + handleTable(document, parent, table, context, null); + + expect(capturedDirection).toBe('rtl'); + }); + it('handleTable without cache', () => { const parent = document.createElement('div'); const tableModel = createTable(1); diff --git a/packages/roosterjs-content-model-dom/test/testUtils.ts b/packages/roosterjs-content-model-dom/test/testUtils.ts index 986f69ebfc10..1402accf7c46 100644 --- a/packages/roosterjs-content-model-dom/test/testUtils.ts +++ b/packages/roosterjs-content-model-dom/test/testUtils.ts @@ -21,14 +21,14 @@ export function createRange(node1: Node, offset1?: number, node2?: Node, offset2 return range; } -declare var __karma__: any; - export function itChromeOnly( expectation: string, assertion?: jasmine.ImplementationCallback, timeout?: number ) { - const func = __karma__.config.browser == 'Chrome' ? it : xit; + const ua = navigator.userAgent; + const isChrome = /Chrome\//.test(ua) && !/Edg\//.test(ua); + const func = isChrome ? it : xit; return func(expectation, assertion, timeout); } diff --git a/packages/roosterjs-content-model-markdown/lib/markdownToModel/appliers/applyLink.ts b/packages/roosterjs-content-model-markdown/lib/markdownToModel/appliers/applyLink.ts deleted file mode 100644 index fbe4b8b9f2db..000000000000 --- a/packages/roosterjs-content-model-markdown/lib/markdownToModel/appliers/applyLink.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { ContentModelText } from 'roosterjs-content-model-types'; - -/** - * @internal - */ -export function applyLink( - textSegment: ContentModelText, - text: string, - url: string -): ContentModelText { - textSegment.text = text; - textSegment.link = { - dataset: {}, - format: { - href: url, - underline: true, - }, - }; - - return textSegment; -} diff --git a/packages/roosterjs-content-model-markdown/lib/markdownToModel/appliers/applySegmentFormatting.ts b/packages/roosterjs-content-model-markdown/lib/markdownToModel/appliers/applySegmentFormatting.ts index 54cc96b7064f..93d627f37c2c 100644 --- a/packages/roosterjs-content-model-markdown/lib/markdownToModel/appliers/applySegmentFormatting.ts +++ b/packages/roosterjs-content-model-markdown/lib/markdownToModel/appliers/applySegmentFormatting.ts @@ -1,13 +1,11 @@ import { adjustHeading } from '../utils/adjustHeading'; -import { applyLink } from './applyLink'; -import { applyTextFormatting } from './applyTextFormatting'; -import { createBr, createText } from 'roosterjs-content-model-dom'; -import { createImageSegment } from '../creators/createImageSegment'; -import { splitParagraphSegments } from '../utils/splitParagraphSegments'; +import { createBr } from 'roosterjs-content-model-dom'; +import { parseInlineSegments } from '../utils/parseInlineSegments'; import type { ContentModelParagraph, ContentModelParagraphDecorator, + ContentModelSegment, } from 'roosterjs-content-model-types'; /** @@ -22,22 +20,20 @@ export function applySegmentFormatting( const br = createBr(); paragraph.segments.push(br); } else { - const textSegments = splitParagraphSegments(text); - for (const segment of textSegments) { - const formattedSegment = createText(segment.text); - if (segment.type === 'image') { - const image = createImageSegment(segment.text, segment.url); - paragraph.segments.push(image); - } else { - if (segment.type === 'link') { - applyLink(formattedSegment, segment.text, segment.url); - } - const segmentWithAdjustedHeading = adjustHeading(formattedSegment, decorator); - if (segmentWithAdjustedHeading) { - const formattedSegments = applyTextFormatting(formattedSegment); - paragraph.segments.push(...formattedSegments); + const segments: ContentModelSegment[] = []; + parseInlineSegments(text, segments); + + // Apply heading adjustment to the first text-bearing segment, if any. + let headingAdjusted = false; + for (const segment of segments) { + if (!headingAdjusted && segment.segmentType === 'Text') { + const adjusted = adjustHeading(segment, decorator); + headingAdjusted = true; + if (!adjusted) { + continue; } } + paragraph.segments.push(segment); } } diff --git a/packages/roosterjs-content-model-markdown/lib/markdownToModel/appliers/applyTextFormatting.ts b/packages/roosterjs-content-model-markdown/lib/markdownToModel/appliers/applyTextFormatting.ts deleted file mode 100644 index d3e1baf8e126..000000000000 --- a/packages/roosterjs-content-model-markdown/lib/markdownToModel/appliers/applyTextFormatting.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { createText } from 'roosterjs-content-model-dom'; -import type { - ContentModelLink, - ContentModelSegmentFormat, - ContentModelText, -} from 'roosterjs-content-model-types'; - -interface FormattingState { - bold: boolean; - italic: boolean; - strikethrough: boolean; -} - -interface FormatMarker { - type: 'bold' | 'italic' | 'strikethrough'; - length: number; -} - -/** - * @internal - */ -export function applyTextFormatting(textSegment: ContentModelText) { - const text = textSegment.text; - - // Quick check: if the text contains only formatting markers, return original - if (isOnlyFormattingMarkers(text)) { - return [textSegment]; - } - - const textSegments: ContentModelText[] = []; - const currentState: FormattingState = { bold: false, italic: false, strikethrough: false }; - - let currentText = ''; - let i = 0; - - while (i < text.length) { - const marker = parseMarkerAt(text, i); - - if (marker) { - // Check if this marker should be treated as formatting or as literal text - if (shouldToggleFormatting(text, i, marker, currentState)) { - // If we have accumulated text, create a segment for it - if (currentText.length > 0) { - textSegments.push( - createFormattedSegment( - currentText, - textSegment.format, - currentState, - textSegment.link - ) - ); - currentText = ''; - } - - // Toggle the formatting state - toggleFormatting(currentState, marker.type); - - // Skip the marker characters - i += marker.length; - } else { - // Treat as regular text if marker is not valid in this context - currentText += text[i]; - i++; - } - } else { - // Regular character, add to current text - currentText += text[i]; - i++; - } - } - - // Add any remaining text as a final segment - if (currentText.length > 0) { - textSegments.push( - createFormattedSegment(currentText, textSegment.format, currentState, textSegment.link) - ); - } - - // If no meaningful formatting was applied, return the original segment - if ( - textSegments.length === 0 || - (textSegments.length === 1 && textSegments[0].text === textSegment.text) - ) { - return [textSegment]; - } - - return textSegments; -} - -function isOnlyFormattingMarkers(text: string): boolean { - // Remove all potential formatting markers and see if anything remains - let remaining = text; - remaining = remaining.replace(/\*\*/g, ''); // Remove ** - remaining = remaining.replace(/~~/g, ''); // Remove ~~ - remaining = remaining.replace(/\*/g, ''); // Remove * - - // If nothing remains after removing all markers, it was only markers - return remaining.length === 0; -} - -function parseMarkerAt(text: string, index: number): FormatMarker | null { - const remaining = text.substring(index); - - if (remaining.startsWith('~~')) { - return { type: 'strikethrough', length: 2 }; - } - - if (remaining.startsWith('**')) { - return { type: 'bold', length: 2 }; - } - - if (remaining.startsWith('*')) { - return { type: 'italic', length: 1 }; - } - - return null; -} - -function shouldToggleFormatting( - text: string, - index: number, - marker: FormatMarker, - currentState: FormattingState -): boolean { - const nextChar = index + marker.length < text.length ? text.charAt(index + marker.length) : ''; - - const isCurrentlyActive = getCurrentFormatState(currentState, marker.type); - - if (isCurrentlyActive) { - // We're currently in this format, so any marker can close it - return true; - } else { - // We're not in this format, so this marker would open it - // Opening markers must be followed by non-whitespace - return nextChar.length > 0 && !isWhitespace(nextChar); - } -} - -function isWhitespace(char: string): boolean { - return /\s/.test(char); -} - -function toggleFormatting(state: FormattingState, type: 'bold' | 'italic' | 'strikethrough'): void { - switch (type) { - case 'bold': - state.bold = !state.bold; - break; - case 'italic': - state.italic = !state.italic; - break; - case 'strikethrough': - state.strikethrough = !state.strikethrough; - break; - } -} - -function getCurrentFormatState( - state: FormattingState, - type: 'bold' | 'italic' | 'strikethrough' -): boolean { - switch (type) { - case 'bold': - return state.bold; - case 'italic': - return state.italic; - case 'strikethrough': - return state.strikethrough; - } -} - -function createFormattedSegment( - text: string, - baseFormat: ContentModelSegmentFormat, - state: FormattingState, - link?: ContentModelLink -): ContentModelText { - const format: ContentModelSegmentFormat = { ...baseFormat }; - - if (state.bold) { - format.fontWeight = 'bold'; - } - - if (state.italic) { - format.italic = true; - } - - if (state.strikethrough) { - format.strikethrough = true; - } - - return createText(text, format, link); -} diff --git a/packages/roosterjs-content-model-markdown/lib/markdownToModel/utils/parseInlineSegments.ts b/packages/roosterjs-content-model-markdown/lib/markdownToModel/utils/parseInlineSegments.ts new file mode 100644 index 000000000000..6ac7c2bc16a2 --- /dev/null +++ b/packages/roosterjs-content-model-markdown/lib/markdownToModel/utils/parseInlineSegments.ts @@ -0,0 +1,212 @@ +import { createImageSegment } from '../creators/createImageSegment'; +import { createText } from 'roosterjs-content-model-dom'; + +import type { + ContentModelLink, + ContentModelSegment, + ContentModelSegmentFormat, + ContentModelText, +} from 'roosterjs-content-model-types'; + +/** + * @internal + */ +interface FormattingState { + bold: boolean; + italic: boolean; + strikethrough: boolean; +} + +/** + * @internal + */ +interface FormatMarker { + type: 'bold' | 'italic' | 'strikethrough'; + length: number; +} + +// Matches a markdown link [text](url) anchored at the start of the input. +const linkPattern = /^\[([^\[\]]+)\]\(([^\)]+)\)/; +// Matches a markdown image ![alt](url) anchored at the start of the input. +const imagePattern = /^!\[([^\[\]]+)\]\(([^\)]+)\)/; + +/** + * @internal + * Parse a markdown inline string into Content Model segments. Supports bold/italic/ + * strikethrough markers, links, and images, and keeps formatting state active across + * link boundaries (e.g. **[link](url)**). + */ +export function parseInlineSegments( + text: string, + segments: ContentModelSegment[], + state: FormattingState = { bold: false, italic: false, strikethrough: false }, + link?: ContentModelLink +) { + let buffer = ''; + let i = 0; + + const flushBuffer = () => { + if (buffer.length > 0) { + segments.push(createFormattedSegment(buffer, state, link)); + buffer = ''; + } + }; + + while (i < text.length) { + const remaining = text.substring(i); + + // Image: ![alt](url) + const imgMatch = imagePattern.exec(remaining); + if (imgMatch && isValidUrl(imgMatch[2])) { + flushBuffer(); + segments.push(createImageSegment(imgMatch[1], imgMatch[2])); + i += imgMatch[0].length; + continue; + } + + // Link: [text](url) — keep outer formatting state active inside the link + const linkMatch = linkPattern.exec(remaining); + if (linkMatch && isValidUrl(linkMatch[2])) { + flushBuffer(); + const innerLink: ContentModelLink = { + dataset: {}, + format: { href: linkMatch[2], underline: true }, + }; + parseInlineSegments(linkMatch[1], segments, state, innerLink); + i += linkMatch[0].length; + continue; + } + + // Formatting marker + const marker = parseMarkerAt(text, i); + if (marker && shouldToggleFormatting(text, i, marker, state)) { + flushBuffer(); + toggleFormatting(state, marker.type); + i += marker.length; + continue; + } + + buffer += text[i]; + i++; + } + + flushBuffer(); +} + +function parseMarkerAt(text: string, index: number): FormatMarker | null { + const remaining = text.substring(index); + + if (remaining.startsWith('~~')) { + return { type: 'strikethrough', length: 2 }; + } + + if (remaining.startsWith('**')) { + return { type: 'bold', length: 2 }; + } + + if (remaining.startsWith('*')) { + return { type: 'italic', length: 1 }; + } + + return null; +} + +function shouldToggleFormatting( + text: string, + index: number, + marker: FormatMarker, + currentState: FormattingState +): boolean { + const isCurrentlyActive = getCurrentFormatState(currentState, marker.type); + + if (isCurrentlyActive) { + return true; + } + + // Opening marker must be followed by a non-whitespace character. + const nextIndex = index + marker.length; + const nextChar = nextIndex < text.length ? text.charAt(nextIndex) : ''; + + if (nextChar.length === 0 || isWhitespace(nextChar)) { + return false; + } + + return true; +} + +function isWhitespace(char: string): boolean { + return /\s/.test(char); +} + +function toggleFormatting(state: FormattingState, type: 'bold' | 'italic' | 'strikethrough'): void { + switch (type) { + case 'bold': + state.bold = !state.bold; + break; + case 'italic': + state.italic = !state.italic; + break; + case 'strikethrough': + state.strikethrough = !state.strikethrough; + break; + } +} + +function getCurrentFormatState( + state: FormattingState, + type: 'bold' | 'italic' | 'strikethrough' +): boolean { + switch (type) { + case 'bold': + return state.bold; + case 'italic': + return state.italic; + case 'strikethrough': + return state.strikethrough; + } +} + +function createFormattedSegment( + text: string, + state: FormattingState, + link?: ContentModelLink +): ContentModelText { + const format: ContentModelSegmentFormat = {}; + + if (state.bold) { + format.fontWeight = 'bold'; + } + + if (state.italic) { + format.italic = true; + } + + if (state.strikethrough) { + format.strikethrough = true; + } + + return createText(text, format, link); +} + +function isValidUrl(url: string): boolean { + if (!url) { + return false; + } + + if ( + url.startsWith('data:') || + url.startsWith('blob:') || + url.startsWith('/') || + url.startsWith('./') || + url.startsWith('../') + ) { + return true; + } + + try { + const parsed = new URL(url); + return parsed.protocol === 'http:' || parsed.protocol === 'https:'; + } catch (_) { + return false; + } +} diff --git a/packages/roosterjs-content-model-markdown/lib/markdownToModel/utils/splitParagraphSegments.ts b/packages/roosterjs-content-model-markdown/lib/markdownToModel/utils/splitParagraphSegments.ts deleted file mode 100644 index 1aca3f61713a..000000000000 --- a/packages/roosterjs-content-model-markdown/lib/markdownToModel/utils/splitParagraphSegments.ts +++ /dev/null @@ -1,87 +0,0 @@ -// Matches markdown links and images in a string. -// Group 1 (full link): [text](url) e.g. [Click here](https://example.com) -// Group 2: link text e.g. "Click here" -// Group 3: link url e.g. "https://example.com" -// Group 4 (full image): ![alt](url) e.g. ![Logo](https://example.com/logo.png) -// Group 5: alt text e.g. "Logo" -// Group 6: image url e.g. "https://example.com/logo.png" -const linkRegex = /(\[([^\[]+)\]\(([^\)]+)\))|(\!\[([^\[]+)\]\(([^\)]+)\))/g; - -/** - * @internal - */ -interface MarkdownSegment { - text: string; - url: string; - type: 'text' | 'link' | 'image'; -} - -const isValidUrl = (url: string) => { - if (!url) { - return false; - } - - // Accept common non-http schemes and relative paths - if ( - url.startsWith('data:') || - url.startsWith('blob:') || - url.startsWith('/') || - url.startsWith('./') || - url.startsWith('../') - ) { - return true; - } - - try { - const parsed = new URL(url); - return parsed.protocol === 'http:' || parsed.protocol === 'https:'; - } catch (_) { - return false; - } -}; - -function pushText(result: MarkdownSegment[], text: string) { - const last = result[result.length - 1]; - if (last && last.type === 'text') { - last.text += text; - } else { - result.push({ type: 'text', text, url: '' }); - } -} - -/** - * @internal - */ -export function splitParagraphSegments(text: string): MarkdownSegment[] { - const result: MarkdownSegment[] = []; - let lastIndex = 0; - let match: RegExpExecArray | null = null; - - while ((match = linkRegex.exec(text)) !== null) { - if (match.index > lastIndex) { - pushText(result, text.slice(lastIndex, match.index)); - } - - if (match[2] && match[3]) { - if (isValidUrl(match[3])) { - result.push({ type: 'link', text: match[2], url: match[3] }); - } else { - pushText(result, match[0]); - } - } else if (match[5] && match[6]) { - if (isValidUrl(match[6])) { - result.push({ type: 'image', text: match[5], url: match[6] }); - } else { - pushText(result, match[0]); - } - } - - lastIndex = linkRegex.lastIndex; - } - - if (lastIndex < text.length) { - pushText(result, text.slice(lastIndex)); - } - - return result; -} diff --git a/packages/roosterjs-content-model-markdown/lib/modelToMarkdown/creators/createMarkdownParagraph.ts b/packages/roosterjs-content-model-markdown/lib/modelToMarkdown/creators/createMarkdownParagraph.ts index 6a082dcf770a..75d73c3bcebb 100644 --- a/packages/roosterjs-content-model-markdown/lib/modelToMarkdown/creators/createMarkdownParagraph.ts +++ b/packages/roosterjs-content-model-markdown/lib/modelToMarkdown/creators/createMarkdownParagraph.ts @@ -47,18 +47,33 @@ export function createMarkdownParagraph( } function textProcessor(text: ContentModelText): string { - let markdownString = text.text; - if (text.link) { - markdownString = `[${text.text}](${text.link.format.href})`; + const { fontWeight, italic, strikethrough } = text.format; + const hasInlineFormat = fontWeight == 'bold' || italic || strikethrough; + + if (!hasInlineFormat) { + return text.link ? `[${text.text}](${text.link.format.href})` : text.text; } - if (text.format.fontWeight == 'bold') { - markdownString = `**${markdownString}**`; + + // Move leading/trailing whitespace outside the markers so the emitted + // markdown is valid (CommonMark requires emphasis markers to hug + // non-whitespace), e.g. "world " with <b> => " " + "**world**". + const match = /^(\s*)([\s\S]*?)(\s*)$/.exec(text.text); + const [, leading, core, trailing] = match ? match : ['', '', text.text, '']; + + if (!core) { + return text.text; } - if (text.format.strikethrough) { - markdownString = `~~${markdownString}~~`; + + let inner = text.link ? `[${core}](${text.link.format.href})` : core; + if (fontWeight == 'bold') { + inner = `**${inner}**`; } - if (text.format.italic) { - markdownString = `*${markdownString}*`; + if (strikethrough) { + inner = `~~${inner}~~`; } - return markdownString; + if (italic) { + inner = `*${inner}*`; + } + + return `${leading}${inner}${trailing}`; } diff --git a/packages/roosterjs-content-model-markdown/test/markdownToModel/appliers/applyLinkTest.ts b/packages/roosterjs-content-model-markdown/test/markdownToModel/appliers/applyLinkTest.ts deleted file mode 100644 index db66571450fb..000000000000 --- a/packages/roosterjs-content-model-markdown/test/markdownToModel/appliers/applyLinkTest.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { applyLink } from '../../../lib/markdownToModel/appliers/applyLink'; -import { ContentModelText } from 'roosterjs-content-model-types'; -import { createText } from 'roosterjs-content-model-dom'; - -describe('applyLink', () => { - function runTest(text: string, url: string, expectedSegment: ContentModelText) { - const textSegment = createText(text); - const result = applyLink(textSegment, text, url); - expect(result).toEqual(expectedSegment); - } - - it('should apply link to text segment', () => { - const linkSegment = createText('link'); - linkSegment.link = { - dataset: {}, - format: { - href: 'https://www.example.com', - underline: true, - }, - }; - runTest('link', 'https://www.example.com', linkSegment); - }); - - it('should apply link to text segment with space in text', () => { - const linkSegmentWith = createText('link with space'); - linkSegmentWith.link = { - dataset: {}, - format: { - href: 'https://www.example.com', - underline: true, - }, - }; - runTest('link with space', 'https://www.example.com', linkSegmentWith); - }); -}); diff --git a/packages/roosterjs-content-model-markdown/test/markdownToModel/appliers/applySegmentFormattingTest.ts b/packages/roosterjs-content-model-markdown/test/markdownToModel/appliers/applySegmentFormattingTest.ts index ef3ec91268a8..b3680d3f0ab3 100644 --- a/packages/roosterjs-content-model-markdown/test/markdownToModel/appliers/applySegmentFormattingTest.ts +++ b/packages/roosterjs-content-model-markdown/test/markdownToModel/appliers/applySegmentFormattingTest.ts @@ -98,6 +98,109 @@ describe('applySegmentFormatting', () => { runTest('text with ![image](http://image.com)', paragraph); }); + it('Bold inside link', () => { + const paragraph = createParagraph(); + const segment = createText('text with '); + const link = createText('bold link', { fontWeight: 'bold' }); + link.link = { + dataset: {}, + format: { + href: 'http://link.com', + underline: true, + }, + }; + paragraph.segments.push(segment); + paragraph.segments.push(link); + runTest('text with [**bold link**](http://link.com)', paragraph); + }); + + it('Italic inside link', () => { + const paragraph = createParagraph(); + const segment = createText('text with '); + const link = createText('italic link', { italic: true }); + link.link = { + dataset: {}, + format: { + href: 'http://link.com', + underline: true, + }, + }; + paragraph.segments.push(segment); + paragraph.segments.push(link); + runTest('text with [*italic link*](http://link.com)', paragraph); + }); + + it('Strikethrough inside link', () => { + const paragraph = createParagraph(); + const segment = createText('text with '); + const link = createText('strike link', { strikethrough: true }); + link.link = { + dataset: {}, + format: { + href: 'http://link.com', + underline: true, + }, + }; + paragraph.segments.push(segment); + paragraph.segments.push(link); + runTest('text with [~~strike link~~](http://link.com)', paragraph); + }); + + it('Mixed formatting inside link', () => { + const paragraph = createParagraph(); + const segment = createText('text with '); + const before = createText('start '); + before.link = { + dataset: {}, + format: { href: 'http://link.com', underline: true }, + }; + const bold = createText('bold', { fontWeight: 'bold' }); + bold.link = { + dataset: {}, + format: { href: 'http://link.com', underline: true }, + }; + const after = createText(' end'); + after.link = { + dataset: {}, + format: { href: 'http://link.com', underline: true }, + }; + paragraph.segments.push(segment); + paragraph.segments.push(before); + paragraph.segments.push(bold); + paragraph.segments.push(after); + runTest('text with [start **bold** end](http://link.com)', paragraph); + }); + + it('Bold around link', () => { + const paragraph = createParagraph(); + const segment = createText('text with '); + const link = createText('link', { fontWeight: 'bold' }); + link.link = { + dataset: {}, + format: { href: 'http://link.com', underline: true }, + }; + paragraph.segments.push(segment); + paragraph.segments.push(link); + runTest('text with **[link](http://link.com)**', paragraph); + }); + + it('Bold continues into link', () => { + const paragraph = createParagraph(); + const segment = createText('text with '); + const boldText = createText('bold ', { fontWeight: 'bold' }); + const link = createText('link', { fontWeight: 'bold' }); + link.link = { + dataset: {}, + format: { href: 'http://link.com', underline: true }, + }; + const trailing = createText(' tail', { fontWeight: 'bold' }); + paragraph.segments.push(segment); + paragraph.segments.push(boldText); + paragraph.segments.push(link); + paragraph.segments.push(trailing); + runTest('text with **bold [link](http://link.com) tail**', paragraph); + }); + it('Complex paragraph with Image, Links, bold and italic', () => { const paragraph = createParagraph(); const segment1 = createText('text with '); diff --git a/packages/roosterjs-content-model-markdown/test/markdownToModel/appliers/applyTextFormattingTest.ts b/packages/roosterjs-content-model-markdown/test/markdownToModel/appliers/applyTextFormattingTest.ts deleted file mode 100644 index 47caec64215f..000000000000 --- a/packages/roosterjs-content-model-markdown/test/markdownToModel/appliers/applyTextFormattingTest.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { applyTextFormatting } from '../../../lib/markdownToModel/appliers/applyTextFormatting'; -import { ContentModelText } from 'roosterjs-content-model-types'; -import { createText } from 'roosterjs-content-model-dom'; - -describe('applyTextFormatting', () => { - function runTest(text: string, expectedSegments: ContentModelText[]) { - // Arrange - const textSegment = createText(text); - - // Act - const result = applyTextFormatting(textSegment); - - // Assert - expect(result).toEqual(expectedSegments); - } - - // Basic functionality verification - it('Simple bold test', () => { - runTest('**bold**', [createText('bold', { fontWeight: 'bold' })]); - }); - - it('Simple italic test', () => { - runTest('*italic*', [createText('italic', { italic: true })]); - }); - - it('Opening marker with space should not format', () => { - const originalSegment = createText('** bold**'); - runTest('** bold**', [originalSegment]); - }); - - it('Closing marker with space should still format', () => { - runTest('**bold **', [createText('bold ', { fontWeight: 'bold' })]); - }); - - it('No formatting ', () => { - const textSegment = createText('No formatting '); - runTest('No formatting ', [textSegment]); - }); - - it('Bold', () => { - runTest('text in **bold**', [ - createText('text in '), - createText('bold', { fontWeight: 'bold' }), - ]); - }); - - it('Italic', () => { - runTest('text in *italic*', [ - createText('text in '), - createText('italic', { italic: true }), - ]); - }); - - it('Strikethrough', () => { - runTest('text in ~~strikethrough~~', [ - createText('text in '), - createText('strikethrough', { strikethrough: true }), - ]); - }); - - it('Bold and Italic', () => { - runTest('text in ***bold and italic***', [ - createText('text in '), - createText('bold and italic', { fontWeight: 'bold', italic: true }), - ]); - }); - - it('Multiple Bold and Italic and Strikethrough', () => { - runTest('text in ***bold and italic*** and **bold** and *italic*', [ - createText('text in '), - createText('bold and italic', { fontWeight: 'bold', italic: true }), - createText(' and '), - createText('bold', { fontWeight: 'bold' }), - createText(' and '), - createText('italic', { italic: true }), - ]); - }); - - // Corner case tests - it('Nested formatting - italic inside bold', () => { - runTest('**bold *italic* bold**', [ - createText('bold ', { fontWeight: 'bold' }), - createText('italic', { fontWeight: 'bold', italic: true }), - createText(' bold', { fontWeight: 'bold' }), - ]); - }); - - it('Nested formatting - bold inside italic', () => { - runTest('*italic **bold** italic*', [ - createText('italic ', { italic: true }), - createText('bold', { italic: true, fontWeight: 'bold' }), - createText(' italic', { italic: true }), - ]); - }); - - it('Complex nested formatting', () => { - runTest('***a*bcd*e*fgh**', [ - createText('a', { fontWeight: 'bold', italic: true }), - createText('bcd', { fontWeight: 'bold' }), - createText('e', { fontWeight: 'bold', italic: true }), - createText('fgh', { fontWeight: 'bold' }), - ]); - }); - - it('Strikethrough with nested formatting', () => { - runTest('~~strike **bold** strike~~', [ - createText('strike ', { strikethrough: true }), - createText('bold', { strikethrough: true, fontWeight: 'bold' }), - createText(' strike', { strikethrough: true }), - ]); - }); - - it('All three formats nested', () => { - runTest('***bold italic ~~strike~~***', [ - createText('bold italic ', { fontWeight: 'bold', italic: true }), - createText('strike', { fontWeight: 'bold', italic: true, strikethrough: true }), - ]); - }); - - it('Overlapping formats', () => { - runTest('**bold ~~strike** end~~', [ - createText('bold ', { fontWeight: 'bold' }), - createText('strike', { fontWeight: 'bold', strikethrough: true }), - createText(' end', { strikethrough: true }), - ]); - }); - - it('Multiple consecutive markers', () => { - runTest('****bold****', [createText('bold')]); - }); - - it('Unmatched opening markers', () => { - runTest('**bold without close', [createText('bold without close', { fontWeight: 'bold' })]); - }); - - it('Unmatched closing markers', () => { - runTest('close without open**', [createText('close without open**')]); - }); - - it('Empty formatting', () => { - const originalSegment = createText('****'); - runTest('****', [originalSegment]); - }); - - it('Single asterisk', () => { - const originalSegment = createText('*'); - runTest('*', [originalSegment]); - }); - - it('Adjacent different formats', () => { - runTest('**bold***italic*~~strike~~', [ - createText('bold', { fontWeight: 'bold' }), - createText('italic', { italic: true }), - createText('strike', { strikethrough: true }), - ]); - }); - - it('Interleaved formats', () => { - runTest('**bold ~~strike** more~~ end', [ - createText('bold ', { fontWeight: 'bold' }), - createText('strike', { fontWeight: 'bold', strikethrough: true }), - createText(' more', { strikethrough: true }), - createText(' end'), - ]); - }); - - it('Triple nested formats', () => { - runTest('***bold italic ~~all three~~ italic bold***', [ - createText('bold italic ', { fontWeight: 'bold', italic: true }), - createText('all three', { fontWeight: 'bold', italic: true, strikethrough: true }), - createText(' italic bold', { fontWeight: 'bold', italic: true }), - ]); - }); - - it('Format at start and end', () => { - runTest('**start** middle ~~end~~', [ - createText('start', { fontWeight: 'bold' }), - createText(' middle '), - createText('end', { strikethrough: true }), - ]); - }); - - it('Only formatting markers', () => { - const originalSegment = createText('******~~~~~~'); - runTest('******~~~~~~', [originalSegment]); - }); - - it('Mixed markers without content', () => { - const originalSegment = createText('***~~***~~'); - runTest('***~~***~~', [originalSegment]); - }); - - it('Many consecutive markers without content', () => { - const originalSegment = createText('**********~~~~~~~~~~'); - runTest('**********~~~~~~~~~~', [originalSegment]); - }); - - it('Alternating markers without content', () => { - // ~ characters are text content, not formatting markers - // This should create alternating italic formatting on the ~ characters - runTest('*~*~*~*~', [ - createText('~', { italic: true }), - createText('~'), - createText('~', { italic: true }), - createText('~'), - ]); - }); - - it('Content between same markers', () => { - runTest('**bold** **more bold**', [ - createText('bold', { fontWeight: 'bold' }), - createText(' '), - createText('more bold', { fontWeight: 'bold' }), - ]); - }); - - it('Partial markers', () => { - // The * is followed by a space, so it should not open formatting - const originalSegment = createText('text with * single asterisk and ~ single tilde'); - runTest('text with * single asterisk and ~ single tilde', [originalSegment]); - }); - - it('Escaped-like patterns (not actually escaped)', () => { - runTest('\\**not bold\\**', [ - createText('\\'), - createText('not bold\\', { fontWeight: 'bold' }), - ]); - }); - - it('Complex realistic example', () => { - runTest('This is **bold** and *italic* and ~~strikethrough~~ and ***all bold italic***', [ - createText('This is '), - createText('bold', { fontWeight: 'bold' }), - createText(' and '), - createText('italic', { italic: true }), - createText(' and '), - createText('strikethrough', { strikethrough: true }), - createText(' and '), - createText('all bold italic', { fontWeight: 'bold', italic: true }), - ]); - }); - - // Whitespace validation tests for proper Markdown compliance - describe('Whitespace validation tests', () => { - it('Opening marker followed by space should not format - asterisk', () => { - const originalSegment = createText('* hello*'); - runTest('* hello*', [originalSegment]); - }); - - it('Opening marker followed by space should not format - double asterisk', () => { - const originalSegment = createText('** hello**'); - runTest('** hello**', [originalSegment]); - }); - - it('Opening marker followed by space should not format - strikethrough', () => { - const originalSegment = createText('~~ hello~~'); - runTest('~~ hello~~', [originalSegment]); - }); - - it('Closing marker preceded by space should still format - asterisk', () => { - runTest('*hello *', [createText('hello ', { italic: true })]); - }); - - it('Closing marker preceded by space should still format - double asterisk', () => { - runTest('**hello **', [createText('hello ', { fontWeight: 'bold' })]); - }); - - it('Closing marker preceded by space should still format - strikethrough', () => { - runTest('~~hello ~~', [createText('hello ', { strikethrough: true })]); - }); - - it('Both markers surrounded by spaces - should not format due to invalid opening', () => { - const originalSegment = createText('** hello **'); - runTest('** hello **', [originalSegment]); - }); - - it('Mixed valid and invalid markers due to spaces', () => { - runTest('**valid** but ** invalid ** and *also valid*', [ - createText('valid', { fontWeight: 'bold' }), - createText(' but ** invalid ** and '), - createText('also valid', { italic: true }), - ]); - }); - - it('Tab character should be treated as whitespace', () => { - const originalSegment = createText('**\thello**'); - runTest('**\thello**', [originalSegment]); - }); - - it('Newline character should be treated as whitespace', () => { - const originalSegment = createText('**\nhello**'); - runTest('**\nhello**', [originalSegment]); - }); - - it('Multiple whitespace characters - opening invalid', () => { - const originalSegment = createText('** hello **'); - runTest('** hello **', [originalSegment]); - }); - - it('Valid formatting with no spaces', () => { - runTest('**bold**and*italic*and~~strike~~', [ - createText('bold', { fontWeight: 'bold' }), - createText('and'), - createText('italic', { italic: true }), - createText('and'), - createText('strike', { strikethrough: true }), - ]); - }); - - it('Valid formatting with punctuation but no spaces', () => { - runTest('**bold!** and *italic,* and ~~strike.~~', [ - createText('bold!', { fontWeight: 'bold' }), - createText(' and '), - createText('italic,', { italic: true }), - createText(' and '), - createText('strike.', { strikethrough: true }), - ]); - }); - - it('Partial valid formatting - opening valid, closing with space still valid', () => { - runTest('**hello ** world', [ - createText('hello ', { fontWeight: 'bold' }), - createText(' world'), - ]); - }); - - it('Partial valid formatting - opening invalid, closing valid', () => { - const originalSegment = createText('** hello** world'); - runTest('** hello** world', [originalSegment]); - }); - - it('Nested formatting with space validation', () => { - runTest('**bold *italic* bold**', [ - createText('bold ', { fontWeight: 'bold' }), - createText('italic', { fontWeight: 'bold', italic: true }), - createText(' bold', { fontWeight: 'bold' }), - ]); - }); - - it('Nested formatting with invalid inner due to opening spaces', () => { - runTest('**bold * invalid * bold**', [ - createText('bold * invalid * bold', { fontWeight: 'bold' }), - ]); - }); - - it('Multiple consecutive spaces around markers - opening invalid', () => { - const originalSegment = createText('** hello **'); - runTest('** hello **', [originalSegment]); - }); - - it('Mixed whitespace types - opening invalid', () => { - const originalSegment = createText('** \t\nhello\t \n**'); - runTest('** \t\nhello\t \n**', [originalSegment]); - }); - - it('Valid opening with space in middle but valid closing', () => { - runTest('**hello world**', [createText('hello world', { fontWeight: 'bold' })]); - }); - - it('Complex scenario with mixed valid and invalid patterns', () => { - runTest('Start **valid** then ** invalid ** then *good* and * bad * end', [ - createText('Start '), - createText('valid', { fontWeight: 'bold' }), - createText(' then ** invalid ** then '), - createText('good', { italic: true }), - createText(' and * bad * end'), - ]); - }); - - it('Strikethrough with space validation edge cases', () => { - runTest('~~valid~~ but ~~ invalid ~~ text', [ - createText('valid', { strikethrough: true }), - createText(' but ~~ invalid ~~ text'), - ]); - }); - - it('All three formats with space validation', () => { - runTest('**b** ~~ i ~~ *t* and ** bad ** ~~bad ~~ * bad *', [ - createText('b', { fontWeight: 'bold' }), - createText(' ~~ i ~~ '), - createText('t', { italic: true }), - createText(' and ** bad ** '), - createText('bad ', { strikethrough: true }), - createText(' * bad *'), - ]); - }); - - // Additional edge cases for closing behavior - it('Valid opening followed by space in closing should still work', () => { - runTest('**hello **', [createText('hello ', { fontWeight: 'bold' })]); - }); - - it('Mixed scenarios with spaces affecting only opening', () => { - // The second * is followed by a space, so it should not open formatting - runTest('*valid* but * invalid opening but valid closing *', [ - createText('valid', { italic: true }), - createText(' but * invalid opening but valid closing *'), - ]); - }); - - it('Comprehensive whitespace rule test', () => { - // Opening markers followed by space = invalid - // Closing markers preceded by space = still valid - runTest('** invalid** but **valid ** and * invalid* but *valid *', [ - createText('** invalid** but '), - createText('valid ', { fontWeight: 'bold' }), - createText(' and * invalid* but '), - createText('valid ', { italic: true }), - ]); - }); - - it('Only markers without content should return original', () => { - const originalSegment = createText('**~~**~~'); - runTest('**~~**~~', [originalSegment]); - }); - - it('Markers with only spaces should return original', () => { - const originalSegment = createText('** ** ~~ ~~'); - runTest('** ** ~~ ~~', [originalSegment]); - }); - - it('Simple marker-only case verification', () => { - const originalSegment = createText('****'); - runTest('****', [originalSegment]); - }); - - it('Mixed marker patterns with no content', () => { - const originalSegment = createText('*~~***~~*'); - runTest('*~~***~~*', [originalSegment]); - }); - - it('Very long marker sequence', () => { - const originalSegment = createText('**************~~~~~~~~~~~~~~'); - runTest('**************~~~~~~~~~~~~~~', [originalSegment]); - }); - }); -}); diff --git a/packages/roosterjs-content-model-markdown/test/markdownToModel/utils/parseInlineSegmentsTest.ts b/packages/roosterjs-content-model-markdown/test/markdownToModel/utils/parseInlineSegmentsTest.ts new file mode 100644 index 000000000000..27fdf10161a9 --- /dev/null +++ b/packages/roosterjs-content-model-markdown/test/markdownToModel/utils/parseInlineSegmentsTest.ts @@ -0,0 +1,316 @@ +import { parseInlineSegments } from '../../../lib/markdownToModel/utils/parseInlineSegments'; +import type { ContentModelSegment } from 'roosterjs-content-model-types'; + +describe('parseInlineSegments', () => { + function runTest(text: string, expected: ContentModelSegment[]) { + // Arrange + const segments: ContentModelSegment[] = []; + + // Act + parseInlineSegments(text, segments); + + // Assert + expect(segments).toEqual(expected); + } + + it('should return no segments for empty text', () => { + runTest('', []); + }); + + it('should parse plain text', () => { + runTest('hello world', [ + { + segmentType: 'Text', + text: 'hello world', + format: {}, + }, + ]); + }); + + it('should parse bold text', () => { + runTest('**bold**', [ + { + segmentType: 'Text', + text: 'bold', + format: { fontWeight: 'bold' }, + }, + ]); + }); + + it('should parse italic text', () => { + runTest('*italic*', [ + { + segmentType: 'Text', + text: 'italic', + format: { italic: true }, + }, + ]); + }); + + it('should parse strikethrough text', () => { + runTest('~~strike~~', [ + { + segmentType: 'Text', + text: 'strike', + format: { strikethrough: true }, + }, + ]); + }); + + it('should parse mixed plain and bold text', () => { + runTest('hello **world**', [ + { + segmentType: 'Text', + text: 'hello ', + format: {}, + }, + { + segmentType: 'Text', + text: 'world', + format: { fontWeight: 'bold' }, + }, + ]); + }); + + it('should parse nested bold and italic', () => { + runTest('**bold *and italic***', [ + { + segmentType: 'Text', + text: 'bold ', + format: { fontWeight: 'bold' }, + }, + { + segmentType: 'Text', + text: 'and italic', + format: { fontWeight: 'bold', italic: true }, + }, + ]); + }); + + it('should not toggle formatting when marker is followed by whitespace', () => { + runTest('a * b', [ + { + segmentType: 'Text', + text: 'a * b', + format: {}, + }, + ]); + }); + + it('should not toggle formatting when marker is at end of text', () => { + runTest('hello *', [ + { + segmentType: 'Text', + text: 'hello *', + format: {}, + }, + ]); + }); + + it('should parse a markdown link', () => { + runTest('[text](https://example.com)', [ + { + segmentType: 'Text', + text: 'text', + format: {}, + link: { + dataset: {}, + format: { href: 'https://example.com', underline: true }, + }, + }, + ]); + }); + + it('should keep outer formatting state active inside a link', () => { + runTest('**[link](https://example.com)**', [ + { + segmentType: 'Text', + text: 'link', + format: { fontWeight: 'bold' }, + link: { + dataset: {}, + format: { href: 'https://example.com', underline: true }, + }, + }, + ]); + }); + + it('should parse formatting inside a link', () => { + runTest('[**bold**](https://example.com)', [ + { + segmentType: 'Text', + text: 'bold', + format: { fontWeight: 'bold' }, + link: { + dataset: {}, + format: { href: 'https://example.com', underline: true }, + }, + }, + ]); + }); + + it('should ignore link with invalid url', () => { + runTest('[text](javascript:alert(1))', [ + { + segmentType: 'Text', + text: '[text](javascript:alert(1))', + format: {}, + }, + ]); + }); + + it('should accept links with relative urls', () => { + runTest('[text](./page)', [ + { + segmentType: 'Text', + text: 'text', + format: {}, + link: { + dataset: {}, + format: { href: './page', underline: true }, + }, + }, + ]); + }); + + it('should parse a markdown image', () => { + runTest('![alt](https://example.com/img.png)', [ + { + segmentType: 'Image', + src: 'https://example.com/img.png', + alt: 'alt', + format: {}, + dataset: {}, + }, + ]); + }); + + it('should accept images with data url', () => { + runTest('![alt](data:image/png;base64,abc)', [ + { + segmentType: 'Image', + src: 'data:image/png;base64,abc', + alt: 'alt', + format: {}, + dataset: {}, + }, + ]); + }); + + it('should ignore image with invalid url', () => { + runTest('![alt](javascript:alert(1))', [ + { + segmentType: 'Text', + text: '![alt](javascript:alert(1))', + format: {}, + }, + ]); + }); + + it('should parse text mixed with image', () => { + runTest('see ![alt](https://example.com/img.png) here', [ + { + segmentType: 'Text', + text: 'see ', + format: {}, + }, + { + segmentType: 'Image', + src: 'https://example.com/img.png', + alt: 'alt', + format: {}, + dataset: {}, + }, + { + segmentType: 'Text', + text: ' here', + format: {}, + }, + ]); + }); + + it('should parse bold, italic and strikethrough combined', () => { + runTest('**bold** *italic* ~~strike~~', [ + { + segmentType: 'Text', + text: 'bold', + format: { fontWeight: 'bold' }, + }, + { + segmentType: 'Text', + text: ' ', + format: {}, + }, + { + segmentType: 'Text', + text: 'italic', + format: { italic: true }, + }, + { + segmentType: 'Text', + text: ' ', + format: {}, + }, + { + segmentType: 'Text', + text: 'strike', + format: { strikethrough: true }, + }, + ]); + }); + + it('should append to existing segments array', () => { + const segments: ContentModelSegment[] = [{ segmentType: 'Text', text: 'pre ', format: {} }]; + + parseInlineSegments('**bold**', segments); + + expect(segments).toEqual([ + { segmentType: 'Text', text: 'pre ', format: {} }, + { segmentType: 'Text', text: 'bold', format: { fontWeight: 'bold' } }, + ]); + }); + + it('should respect provided initial state', () => { + const segments: ContentModelSegment[] = []; + + parseInlineSegments('hello', segments, { + bold: true, + italic: false, + strikethrough: false, + }); + + expect(segments).toEqual([ + { + segmentType: 'Text', + text: 'hello', + format: { fontWeight: 'bold' }, + }, + ]); + }); + + it('should apply provided link to plain text', () => { + const segments: ContentModelSegment[] = []; + + parseInlineSegments( + 'hello', + segments, + { bold: false, italic: false, strikethrough: false }, + { + dataset: {}, + format: { href: 'https://example.com', underline: true }, + } + ); + + expect(segments).toEqual([ + { + segmentType: 'Text', + text: 'hello', + format: {}, + link: { + dataset: {}, + format: { href: 'https://example.com', underline: true }, + }, + }, + ]); + }); +}); diff --git a/packages/roosterjs-content-model-markdown/test/markdownToModel/utils/splitParagraphSegmentsTest.ts b/packages/roosterjs-content-model-markdown/test/markdownToModel/utils/splitParagraphSegmentsTest.ts deleted file mode 100644 index c3171417916f..000000000000 --- a/packages/roosterjs-content-model-markdown/test/markdownToModel/utils/splitParagraphSegmentsTest.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { splitParagraphSegments } from '../../../lib/markdownToModel/utils/splitParagraphSegments'; - -describe('splitLinksAndImages', () => { - function runTest( - text: string, - expected: { - text: string; - url: string; - type: 'text' | 'link' | 'image'; - }[] - ) { - // Act - const result = splitParagraphSegments(text); - - // Assert - expect(result).toEqual(expected); - } - - it('should return empty array for empty text', () => { - runTest('', []); - }); - - it('should return empty array for text without link or image', () => { - runTest('text without link or image', [ - { text: 'text without link or image', type: 'text', url: '' }, - ]); - }); - - it('should return text with link', () => { - runTest('[link](https://www.example.com)', [ - { text: 'link', type: 'link', url: 'https://www.example.com' }, - ]); - }); - - it('should return text with image', () => { - runTest('![image](https://www.example.com)', [ - { text: 'image', type: 'image', url: 'https://www.example.com' }, - ]); - }); - - it('should return text with link and image', () => { - runTest('[link](https://www.example.com) and ![image](https://www.example.com)', [ - { text: 'link', type: 'link', url: 'https://www.example.com' }, - { text: ' and ', type: 'text', url: '' }, - { text: 'image', type: 'image', url: 'https://www.example.com' }, - ]); - }); - - it('should return text with link and image with space', () => { - runTest( - '[link](https://www.example.com) and ![image with space](https://www.example.com)', - [ - { text: 'link', type: 'link', url: 'https://www.example.com' }, - { text: ' and ', type: 'text', url: '' }, - { text: 'image with space', type: 'image', url: 'https://www.example.com' }, - ] - ); - }); - - it('should return text with link and image with space in link', () => { - runTest( - '[link with space](https://www.example.com/withspace) and ![image](https://www.example.com)', - [ - { text: 'link with space', type: 'link', url: 'https://www.example.com/withspace' }, - { text: ' and ', type: 'text', url: '' }, - { text: 'image', type: 'image', url: 'https://www.example.com' }, - ] - ); - }); - - it('should return only text and when image and link are not valid', () => { - runTest( - '[link](ht3tps://www.example.com/with) and ![image](http3s://www.example.com/with)', - [ - { - text: - '[link](ht3tps://www.example.com/with) and ![image](http3s://www.example.com/with)', - type: 'text', - url: '', - }, - ] - ); - }); - - it('should treat invalid link as text but still render valid image', () => { - runTest('[link](ht3tps://www.example.com) and ![image](https://www.example.com)', [ - { text: '[link](ht3tps://www.example.com) and ', type: 'text', url: '' }, - { text: 'image', type: 'image', url: 'https://www.example.com' }, - ]); - }); - - it('should render valid link but treat invalid image as text', () => { - runTest('[link](https://www.example.com) and ![image](http3s://www.example.com)', [ - { text: 'link', type: 'link', url: 'https://www.example.com' }, - { text: ' and ![image](http3s://www.example.com)', type: 'text', url: '' }, - ]); - }); - - it('should accept data: URL for image', () => { - runTest('![image](data:image/png;base64,abc123)', [ - { text: 'image', type: 'image', url: 'data:image/png;base64,abc123' }, - ]); - }); - - it('should accept blob: URL for image', () => { - runTest('![image](blob:https://example.com/some-id)', [ - { text: 'image', type: 'image', url: 'blob:https://example.com/some-id' }, - ]); - }); - - it('should accept absolute path for link', () => { - runTest('[link](/path/to/page)', [{ text: 'link', type: 'link', url: '/path/to/page' }]); - }); - - it('should accept relative path with ./ for link', () => { - runTest('[link](./relative/path)', [ - { text: 'link', type: 'link', url: './relative/path' }, - ]); - }); - - it('should accept relative path with ../ for link', () => { - runTest('[link](../parent/path)', [{ text: 'link', type: 'link', url: '../parent/path' }]); - }); - - it('should handle text before and after a link', () => { - runTest('before [link](https://www.example.com) after', [ - { text: 'before ', type: 'text', url: '' }, - { text: 'link', type: 'link', url: 'https://www.example.com' }, - { text: ' after', type: 'text', url: '' }, - ]); - }); - - it('should accept http: URL', () => { - runTest('[link](http://www.example.com)', [ - { text: 'link', type: 'link', url: 'http://www.example.com' }, - ]); - }); - - it('should accept URL with query string and fragment', () => { - runTest('[link](https://www.example.com/page?q=1&r=2#section)', [ - { - text: 'link', - type: 'link', - url: 'https://www.example.com/page?q=1&r=2#section', - }, - ]); - }); - - it('should handle two adjacent links with no text between', () => { - runTest('[first](https://www.example.com/1)[second](https://www.example.com/2)', [ - { text: 'first', type: 'link', url: 'https://www.example.com/1' }, - { text: 'second', type: 'link', url: 'https://www.example.com/2' }, - ]); - }); - - it('should treat a single invalid link as plain text', () => { - runTest('[link](ht3tps://www.example.com)', [ - { text: '[link](ht3tps://www.example.com)', type: 'text', url: '' }, - ]); - }); - - it('should treat a single invalid image as plain text', () => { - runTest('![image](http3s://www.example.com)', [ - { text: '![image](http3s://www.example.com)', type: 'text', url: '' }, - ]); - }); - - it('should treat partial markdown syntax as plain text', () => { - runTest('[not a link] and (not a url)', [ - { text: '[not a link] and (not a url)', type: 'text', url: '' }, - ]); - }); - - it('should accept relative path for image', () => { - runTest('![image](./images/photo.png)', [ - { text: 'image', type: 'image', url: './images/photo.png' }, - ]); - }); - - it('should handle multiple images in a row', () => { - runTest( - '![first](https://www.example.com/1.png) ![second](https://www.example.com/2.png)', - [ - { text: 'first', type: 'image', url: 'https://www.example.com/1.png' }, - { text: ' ', type: 'text', url: '' }, - { text: 'second', type: 'image', url: 'https://www.example.com/2.png' }, - ] - ); - }); -}); diff --git a/packages/roosterjs-content-model-markdown/test/modelToMarkdown/convertContentModelToMarkdownTest.ts b/packages/roosterjs-content-model-markdown/test/modelToMarkdown/convertContentModelToMarkdownTest.ts index 8d7fca0d2dae..1a8052fd93eb 100644 --- a/packages/roosterjs-content-model-markdown/test/modelToMarkdown/convertContentModelToMarkdownTest.ts +++ b/packages/roosterjs-content-model-markdown/test/modelToMarkdown/convertContentModelToMarkdownTest.ts @@ -115,6 +115,66 @@ describe('convertContentModelToMarkdown', () => { expect(md).toEqual(markdown); }); + it('should move trailing whitespace outside bold markers (issue 3100)', () => { + const model = createContentModelDocument(); + const paragraph = createParagraph(); + paragraph.segments.push(createText('hello ')); + paragraph.segments.push(createText('world ', { fontWeight: 'bold' })); + paragraph.segments.push(createText('how are you?')); + model.blocks.push(paragraph); + + const md = convertContentModelToMarkdown(model).trim(); + expect(md).toEqual('hello **world** how are you?'); + }); + + it('should move leading whitespace outside italic markers (issue 3100)', () => { + const model = createContentModelDocument(); + const paragraph = createParagraph(); + paragraph.segments.push(createText('hello')); + paragraph.segments.push(createText(' world', { italic: true })); + paragraph.segments.push(createText(' how are you?')); + model.blocks.push(paragraph); + + const md = convertContentModelToMarkdown(model).trim(); + expect(md).toEqual('hello *world* how are you?'); + }); + + it('should move surrounding whitespace outside strikethrough markers', () => { + const model = createContentModelDocument(); + const paragraph = createParagraph(); + paragraph.segments.push(createText('a')); + paragraph.segments.push(createText(' b ', { strikethrough: true })); + paragraph.segments.push(createText('c')); + model.blocks.push(paragraph); + + const md = convertContentModelToMarkdown(model).trim(); + expect(md).toEqual('a ~~b~~ c'); + }); + + it('should move whitespace outside combined bold + italic markers', () => { + const model = createContentModelDocument(); + const paragraph = createParagraph(); + paragraph.segments.push(createText('x')); + paragraph.segments.push(createText(' y ', { fontWeight: 'bold', italic: true })); + paragraph.segments.push(createText('z')); + model.blocks.push(paragraph); + + const md = convertContentModelToMarkdown(model).trim(); + expect(md).toEqual('x ***y*** z'); + }); + + it('should not wrap whitespace-only segments with markers', () => { + const model = createContentModelDocument(); + const paragraph = createParagraph(); + paragraph.segments.push(createText('a')); + paragraph.segments.push(createText(' ', { fontWeight: 'bold' })); + paragraph.segments.push(createText('b')); + model.blocks.push(paragraph); + + const md = convertContentModelToMarkdown(model).trim(); + expect(md).toEqual('a b'); + }); + it('should set a default alt to images', () => { const markdown = '![image](https://www.example.com/image)'; const model = createModelFromHtml("<img src='https://www.example.com/image'>"); diff --git a/packages/roosterjs-content-model-markdown/test/modelToMarkdown/creators/createMarkdownBlockgroupTest.ts b/packages/roosterjs-content-model-markdown/test/modelToMarkdown/creators/createMarkdownBlockgroupTest.ts index 80a07a43c4f0..1ff370d48c0b 100644 --- a/packages/roosterjs-content-model-markdown/test/modelToMarkdown/creators/createMarkdownBlockgroupTest.ts +++ b/packages/roosterjs-content-model-markdown/test/modelToMarkdown/creators/createMarkdownBlockgroupTest.ts @@ -204,7 +204,7 @@ describe('createMarkdownBlockGroup', () => { paragraph.segments.push(text); paragraph.segments.push(linkText); blockGroup.blocks.push(paragraph); - runTest(blockGroup, `> *text *[link](https://example.com)\n\n`, { + runTest(blockGroup, `> *text* [link](https://example.com)\n\n`, { listItemCount: 0, subListItemCount: 0, }); diff --git a/packages/roosterjs-content-model-plugins/test/imageEdit/utils/generateDataURLTest.ts b/packages/roosterjs-content-model-plugins/test/imageEdit/utils/generateDataURLTest.ts index 8fca8e6f14b3..90b744f7a90c 100644 --- a/packages/roosterjs-content-model-plugins/test/imageEdit/utils/generateDataURLTest.ts +++ b/packages/roosterjs-content-model-plugins/test/imageEdit/utils/generateDataURLTest.ts @@ -3,6 +3,9 @@ import { itChromeOnly } from 'roosterjs-content-model-dom/test/testUtils'; describe('generateDataURL', () => { itChromeOnly('generate image url', () => { + const dataUri = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAANElEQVR4AezSsQkAAAjEQHH/oZ0hjdVZPwhHdh7Ok4SMC1cSSGN14UoCaawuXEkgjdWVuA4AAP//YI5Y5AAAAAZJREFUAwAKXgAzAC3ppgAAAABJRU5ErkJggg=='; + spyOn(HTMLCanvasElement.prototype, 'toDataURL').and.returnValue(dataUri); const editInfo = { src: 'test', widthPx: 20, @@ -16,11 +19,8 @@ describe('generateDataURL', () => { angleRad: 0, }; const image = document.createElement('img'); - image.src = 'https://th.bing.com/th/id/OIP.kJCCjl_yUweRlj94AdU-egHaFK?rs=1&pid=ImgDetMain'; const url = generateDataURL(image, editInfo); - expect(url).toBe( - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAChJREFUOE9jZKAyYKSyeQyjBlIeoqNhOBqGZITAaLIhI9DQtIzAMAQASMYAFTvklLAAAAAASUVORK5CYII=' - ); + expect(url).toBe(dataUri); }); itChromeOnly('generate image url - draw image - error', () => { @@ -41,8 +41,6 @@ describe('generateDataURL', () => { image.height = 0; image.src = 'https://th.bing.com/th/id/OIP.kJCCjl_yUweRlj94AdU-egHaFK?rs=1&pid=ImgDetMain'; const url = generateDataURL(image, editInfo); - expect(url).toBe( - 'https://th.bing.com/th/id/OIP.kJCCjl_yUweRlj94AdU-egHaFK?rs=1&pid=ImgDetMain' - ); + expect(url).toBe('data:,'); }); }); diff --git a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts index f56e056ab841..0fb0f41792e9 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts @@ -126,10 +126,10 @@ describe(ID, () => { blockGroupType: 'Document', blocks: [ { - widths: jasmine.anything() as any, + widths: [], rows: [ { - height: jasmine.anything() as any, + height: 0, cells: [ { spanAbove: false, @@ -145,16 +145,18 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '700', textColor: 'black', + fontWeight: '700', }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -188,16 +190,18 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '700', textColor: 'black', + fontWeight: '700', }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -212,6 +216,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '56pt', + height: '28px', }, dataset: {}, }, @@ -229,15 +234,18 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '700', textColor: 'black', + fontWeight: '700', }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', }, + blockType: 'Paragraph', + format: { textAlign: 'center' }, }, ], format: { @@ -251,6 +259,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '62pt', + height: '28px', }, dataset: {}, }, @@ -258,7 +267,7 @@ describe(ID, () => { format: {}, }, { - height: jasmine.anything() as any, + height: 0, cells: [ { spanAbove: false, @@ -278,11 +287,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -296,8 +307,8 @@ describe(ID, () => { paddingRight: '1px', paddingLeft: '1px', verticalAlign: 'middle', - height: '30pt', width: '69.333px', + height: '30pt', }, dataset: {}, }, @@ -327,11 +338,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(5, 99, 193)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -345,6 +358,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '74.667px', + height: '30px', }, dataset: {}, }, @@ -366,11 +380,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(219, 219, 219)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -384,6 +400,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '82.667px', + height: '30px', }, dataset: {}, }, @@ -391,7 +408,7 @@ describe(ID, () => { format: {}, }, { - height: jasmine.anything() as any, + height: 0, cells: [ { spanAbove: false, @@ -411,11 +428,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -429,8 +448,8 @@ describe(ID, () => { paddingRight: '1px', paddingLeft: '1px', verticalAlign: 'middle', - height: '30pt', width: '69.333px', + height: '30pt', }, dataset: {}, }, @@ -460,11 +479,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(5, 99, 193)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -478,6 +499,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '74.667px', + height: '30px', }, dataset: {}, }, @@ -499,11 +521,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(219, 219, 219)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -517,6 +541,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '82.667px', + height: '30px', }, dataset: {}, }, @@ -524,7 +549,7 @@ describe(ID, () => { format: {}, }, { - height: jasmine.anything() as any, + height: 0, cells: [ { spanAbove: false, @@ -544,11 +569,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -562,8 +589,8 @@ describe(ID, () => { paddingRight: '1px', paddingLeft: '1px', verticalAlign: 'middle', - height: '30pt', width: '69.333px', + height: '30pt', }, dataset: {}, }, @@ -593,11 +620,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(5, 99, 193)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -611,6 +640,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '74.667px', + height: '30px', }, dataset: {}, }, @@ -632,11 +662,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(219, 219, 219)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -650,6 +682,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '82.667px', + height: '30px', }, dataset: {}, }, @@ -657,7 +690,7 @@ describe(ID, () => { format: {}, }, { - height: jasmine.anything() as any, + height: 0, cells: [ { spanAbove: false, @@ -677,11 +710,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -695,8 +730,8 @@ describe(ID, () => { paddingRight: '1px', paddingLeft: '1px', verticalAlign: 'middle', - height: '30pt', width: '69.333px', + height: '30pt', }, dataset: {}, }, @@ -726,11 +761,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(5, 99, 193)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -744,6 +781,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '74.667px', + height: '30px', }, dataset: {}, }, @@ -765,11 +803,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(219, 219, 219)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -783,6 +823,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '82.667px', + height: '30px', }, dataset: {}, }, @@ -790,7 +831,7 @@ describe(ID, () => { format: {}, }, { - height: jasmine.anything() as any, + height: 0, cells: [ { spanAbove: false, @@ -810,11 +851,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -828,8 +871,8 @@ describe(ID, () => { paddingRight: '1px', paddingLeft: '1px', verticalAlign: 'middle', - height: '30pt', width: '69.333px', + height: '30pt', }, dataset: {}, }, @@ -859,11 +902,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(5, 99, 193)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -877,6 +922,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '74.667px', + height: '30px', }, dataset: {}, }, @@ -898,11 +944,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(219, 219, 219)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -916,6 +964,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '82.667px', + height: '30px', }, dataset: {}, }, @@ -923,7 +972,7 @@ describe(ID, () => { format: {}, }, { - height: jasmine.anything() as any, + height: 0, cells: [ { spanAbove: false, @@ -943,11 +992,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -961,8 +1012,8 @@ describe(ID, () => { paddingRight: '1px', paddingLeft: '1px', verticalAlign: 'middle', - height: '30pt', width: '69.333px', + height: '30pt', }, dataset: {}, }, @@ -992,11 +1043,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(5, 99, 193)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -1010,6 +1063,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '74.667px', + height: '30px', }, dataset: {}, }, @@ -1031,11 +1085,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(219, 219, 219)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -1049,6 +1105,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '82.667px', + height: '30px', }, dataset: {}, }, @@ -1056,7 +1113,7 @@ describe(ID, () => { format: {}, }, { - height: jasmine.anything() as any, + height: 0, cells: [ { spanAbove: false, @@ -1076,11 +1133,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -1094,8 +1153,8 @@ describe(ID, () => { paddingRight: '1px', paddingLeft: '1px', verticalAlign: 'middle', - height: '30pt', width: '69.333px', + height: '30pt', }, dataset: {}, }, @@ -1125,11 +1184,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(5, 99, 193)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -1143,6 +1204,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '74.667px', + height: '30px', }, dataset: {}, }, @@ -1164,11 +1226,13 @@ describe(ID, () => { }, }, ], - blockType: 'Paragraph', - format: { - textAlign: 'center', - whiteSpace: 'nowrap', + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(219, 219, 219)', }, + blockType: 'Paragraph', + format: { textAlign: 'center', whiteSpace: 'nowrap' }, }, ], format: { @@ -1182,6 +1246,7 @@ describe(ID, () => { paddingLeft: '1px', verticalAlign: 'middle', width: '82.667px', + height: '30px', }, dataset: {}, }, @@ -1191,10 +1256,13 @@ describe(ID, () => { ], blockType: 'Table', format: { - width: jasmine.anything(), + width: '170pt', useBorderBox: true, borderCollapse: true, - } as any, + legacyTableBorder: '0', + cellSpacing: '0', + cellPadding: '0', + }, dataset: {}, }, { @@ -1216,10 +1284,7 @@ describe(ID, () => { underline: false, }, }, - { - segmentType: 'Br', - format: {}, - }, + { segmentType: 'Br', format: {} }, ], blockType: 'Paragraph', format: {}, diff --git a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromGoggleSheetsTest.ts b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromGoggleSheetsTest.ts index b57f2d06d30a..7441e6a6e24c 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromGoggleSheetsTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromGoggleSheetsTest.ts @@ -1857,6 +1857,9 @@ describe('Google Sheets E2E', () => { tableLayout: 'fixed', useBorderBox: true, borderCollapse: true, + legacyTableBorder: '1', + cellSpacing: '0', + cellPadding: '0', }, dataset: { sheetsRoot: '1', sheetsBaot: '1' }, }, diff --git a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts index 9f70c6d9e857..caa9155d94a3 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts @@ -8,6 +8,7 @@ import { wordClipboardContent1, wordClipboardContent2, wordClipboardContent3, + wordClipboardContent4, } from './htmlTemplates/wordClipboardContent'; const ID = 'CM_Paste_From_WORD_E2E'; @@ -46,42 +47,62 @@ describe(ID, () => { }); expect(wordFile.processPastedContentFromWordDesktop).toHaveBeenCalled(); - expect(model).toEqual({ + expectEqual(model, { blockGroupType: 'Document', blocks: [ { - cachedElement: undefined, - isImplicit: undefined, segments: [ { text: 'Test', segmentType: 'Text', - isSelected: undefined, - format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt' }, + format: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, }, ], - segmentFormat: undefined, blockType: 'Paragraph', - format: { marginTop: '0px', marginBottom: '0px' }, + format: { + lineHeight: '1.284', + marginTop: '0in', + marginRight: '0in', + marginBottom: '8pt', + marginLeft: '0in', + }, decorator: { tagName: 'p', format: {} }, }, { - cachedElement: undefined, - isImplicit: undefined, segments: [ { text: 'asdsad', segmentType: 'Text', - isSelected: undefined, - format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt' }, + format: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, }, + ], + blockType: 'Paragraph', + format: { + lineHeight: '1.284', + marginTop: '0in', + marginRight: '0in', + marginBottom: '8pt', + marginLeft: '0in', + }, + decorator: { tagName: 'p', format: {} }, + }, + { + segments: [ { isSelected: true, segmentType: 'SelectionMarker', format: { + backgroundColor: '', fontFamily: '', fontSize: '', - backgroundColor: '', fontWeight: '', italic: false, letterSpacing: '', @@ -92,16 +113,10 @@ describe(ID, () => { underline: false, }, }, + { segmentType: 'Br', format: {} }, ], - segmentFormat: undefined, blockType: 'Paragraph', - format: { - marginTop: '0in', - marginRight: '0in', - marginBottom: '8pt', - marginLeft: '0in', - }, - decorator: { tagName: 'p', format: {} }, + format: {}, }, ], format: {}, @@ -121,33 +136,32 @@ describe(ID, () => { blockGroupType: 'Document', blocks: [ { - widths: [jasmine.anything() as any, jasmine.anything() as any], + widths: [], rows: [ { - height: jasmine.anything() as any, - format: {}, + height: 0, cells: [ { + spanAbove: false, + spanLeft: false, + isHeader: false, blockGroupType: 'TableCell', blocks: [ { - blockType: 'Paragraph', segments: [ { text: 'Asdasdsad', segmentType: 'Text', - format: {}, + format: { textColor: 'rgb(0, 0, 0)' }, }, ], + blockType: 'Paragraph', format: { - lineHeight: 'normal', + lineHeight: '120%', marginTop: '1em', marginBottom: '0in', }, - decorator: { - tagName: 'p', - format: {}, - }, + decorator: { tagName: 'p', format: {} }, }, ], format: { @@ -162,32 +176,29 @@ describe(ID, () => { verticalAlign: 'top', width: '233.75pt', }, - spanLeft: false, - spanAbove: false, - isHeader: false, dataset: {}, }, { + spanAbove: false, + spanLeft: false, + isHeader: false, blockGroupType: 'TableCell', blocks: [ { - blockType: 'Paragraph', segments: [ { text: 'asdadasd', segmentType: 'Text', - format: {}, + format: { textColor: 'rgb(0, 0, 0)' }, }, ], + blockType: 'Paragraph', format: { - lineHeight: 'normal', + lineHeight: '120%', marginTop: '1em', marginBottom: '0in', }, - decorator: { - tagName: 'p', - format: {}, - }, + decorator: { tagName: 'p', format: {} }, }, ], format: { @@ -201,70 +212,55 @@ describe(ID, () => { verticalAlign: 'top', width: '233.75pt', }, - spanLeft: false, - spanAbove: false, - isHeader: false, dataset: {}, }, ], + format: {}, }, ], blockType: 'Table', format: { useBorderBox: true, borderCollapse: true, + legacyTableBorder: '1', + cellSpacing: '0', + cellPadding: '0', }, dataset: {}, }, { - blockType: 'Paragraph', segments: [ - { - segmentType: 'Text', - text: ' ', - format: {}, - }, + { text: ' ', segmentType: 'Text', format: { textColor: 'rgb(0, 0, 0)' } }, ], - format: { - marginTop: '1em', - marginBottom: '1em', - }, - decorator: { - tagName: 'p', - format: {}, - }, + blockType: 'Paragraph', + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, }, { - blockType: 'Paragraph', segments: [ { text: 'asdsadasdasdsadasdsadsad', segmentType: 'Text', - format: { - textColor: 'rgb(0, 0, 0)', - }, + format: { textColor: 'rgb(0, 0, 0)' }, }, ], - format: { - marginTop: '1em', - marginBottom: '1em', - }, - decorator: { - tagName: 'p', - format: {}, - }, + blockType: 'Paragraph', + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, }, { + segments: [ + { text: ' ', segmentType: 'Text', format: { textColor: 'rgb(0, 0, 0)' } }, + ], blockType: 'Paragraph', + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { segments: [ { - segmentType: 'Text', - text: ' ', - format: {}, - }, - { - segmentType: 'SelectionMarker', isSelected: true, + segmentType: 'SelectionMarker', format: { backgroundColor: '', fontFamily: '', @@ -279,15 +275,10 @@ describe(ID, () => { underline: false, }, }, + { segmentType: 'Br', format: {} }, ], - format: { - marginTop: '1em', - marginBottom: '1em', - }, - decorator: { - tagName: 'p', - format: {}, - }, + blockType: 'Paragraph', + format: {}, }, ], format: {}, @@ -852,7 +843,6 @@ describe(ID, () => { paste(editor, wordClipboardContent2); const model = editor.getContentModelCopy('connected'); - navigator.clipboard.writeText(JSON.stringify(model)); expect(wordFile.processPastedContentFromWordDesktop).toHaveBeenCalled(); expectEqual(model, { blockGroupType: 'Document', @@ -1577,6 +1567,9 @@ describe(ID, () => { width: '580.5pt', useBorderBox: true, borderCollapse: true, + legacyTableBorder: '1', + cellSpacing: '0', + cellPadding: '0', }, dataset: {}, }, @@ -1620,4 +1613,703 @@ describe(ID, () => { format: {}, }); }); + + itChromeOnly('E2E paragraphs that are handled as list and with unneeded styles', () => { + paste(editor, wordClipboardContent4); + const model = editor.getContentModelCopy('disconnected'); + + expect(wordFile.processPastedContentFromWordDesktop).toHaveBeenCalled(); + debugger; + expectEqual(model, { + blockGroupType: 'Document', + blocks: [ + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: '■■■■■■■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + blockType: 'Paragraph', + format: {}, + decorator: { tagName: 'h1', format: { fontSize: '2em', fontWeight: 'bold' } }, + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: 1, + marginTop: '1em', + listStyleType: 'decimal', + }, + dataset: { editingInfo: '{"orderedStyleType":1}' }, + }, + ], + blockType: 'BlockGroup', + format: { marginBottom: '2pt' }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + levels: [ + { + listType: 'OL', + format: { marginTop: '1em', listStyleType: 'decimal' }, + dataset: { editingInfo: '{"orderedStyleType":1}' }, + }, + ], + blockType: 'BlockGroup', + format: { marginBottom: '2pt' }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: + '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: '■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + { + text: '■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + link: { format: { name: '_Int_hqC0OfdX' }, dataset: {} }, + }, + { + text: '■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + blockType: 'Paragraph', + format: {}, + decorator: { tagName: 'h1', format: { fontSize: '2em', fontWeight: 'bold' } }, + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + levels: [ + { + listType: 'OL', + format: { marginTop: '1em', listStyleType: 'decimal' }, + dataset: { editingInfo: '{"orderedStyleType":1}' }, + }, + ], + blockType: 'BlockGroup', + format: { marginBottom: '2pt' }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: + '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + levels: [ + { + listType: 'OL', + format: { marginTop: '1em', listStyleType: 'decimal' }, + dataset: { editingInfo: '{"orderedStyleType":1}' }, + }, + ], + blockType: 'BlockGroup', + format: { marginBottom: '2pt' }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: + '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: '■■■■■■', + segmentType: 'Text', + format: { textColor: 'black', italic: true }, + }, + ], + blockType: 'Paragraph', + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: '■■■■■■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { + fontFamily: '"Aptos Light", sans-serif', + textColor: 'rgb(127, 127, 127)', + }, + }, + ], + blockType: 'Paragraph', + format: { + textAlign: 'center', + marginTop: '0in', + marginRight: '0in', + marginBottom: '20pt', + marginLeft: '0.25in', + }, + decorator: { tagName: 'p', format: {} }, + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + blockType: 'Paragraph', + format: {}, + decorator: { tagName: 'h1', format: { fontSize: '2em', fontWeight: 'bold' } }, + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: '■■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '9pt', + textColor: 'rgb(37, 37, 37)', + }, + }, + { + text: '■■■', + segmentType: 'Text', + format: { + fontFamily: 'Aptos, sans-serif', + textColor: 'rgb(52, 171, 255)', + }, + }, + { + text: '■■', + segmentType: 'Text', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '9pt', + textColor: 'rgb(37, 37, 37)', + }, + }, + { + text: '■■■■■■■■■■■', + segmentType: 'Text', + format: { + fontFamily: 'Aptos, sans-serif', + textColor: 'rgb(52, 171, 255)', + }, + }, + { + text: '■■', + segmentType: 'Text', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '9pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + { + text: '■■■■■■', + segmentType: 'Text', + format: { + fontFamily: 'Aptos, sans-serif', + textColor: 'rgb(52, 171, 255)', + }, + }, + ], + blockType: 'Paragraph', + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: '■■■■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + blockType: 'Paragraph', + format: {}, + decorator: { tagName: 'h2', format: { fontSize: '1.5em', fontWeight: 'bold' } }, + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: + '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + blockType: 'Paragraph', + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: '■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + blockType: 'Paragraph', + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + levels: [ + { + listType: 'OL', + format: { marginTop: '1em', listStyleType: 'decimal' }, + dataset: { editingInfo: '{"orderedStyleType":1}' }, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: + '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + levels: [ + { + listType: 'OL', + format: { marginTop: '1em', listStyleType: 'decimal' }, + dataset: { editingInfo: '{"orderedStyleType":1}' }, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: + '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + levels: [ + { + listType: 'OL', + format: { marginTop: '1em', listStyleType: 'decimal' }, + dataset: { editingInfo: '{"orderedStyleType":1}' }, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: '■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + blockType: 'Paragraph', + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: + '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + blockType: 'Paragraph', + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + segments: [ + { + text: '■■■■', + segmentType: 'Text', + format: { textColor: 'rgb(0, 0, 0)' }, + }, + ], + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + backgroundColor: '', + fontFamily: '', + fontSize: '', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: '', + underline: false, + }, + }, + { segmentType: 'Br', format: {} }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + }); + }); }); diff --git a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts index 2bf61310dd1b..3d3ca54c1687 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts @@ -42,29 +42,36 @@ describe(ID, () => { blockGroupType: 'Document', blocks: [ { - blockType: 'Table', + widths: [], rows: [ { - height: <any>jasmine.anything(), - format: {}, + height: 0, cells: [ { + spanAbove: false, + spanLeft: false, + isHeader: false, blockGroupType: 'TableCell', blocks: [ { - blockType: 'Paragraph', segments: [ { - segmentType: 'Text', text: 'No.', + segmentType: 'Text', format: { + textColor: 'black', fontFamily: 'Calibri, sans-serif', fontSize: '11pt', fontWeight: '700', - textColor: 'black', }, }, ], + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', + }, + blockType: 'Paragraph', format: { textAlign: 'center', whiteSpace: 'nowrap', @@ -90,30 +97,33 @@ describe(ID, () => { width: '52pt', height: '28.5pt', }, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: { - ogsb: 'white', - }, + dataset: { ogsb: 'white' }, }, { + spanAbove: false, + spanLeft: false, + isHeader: false, blockGroupType: 'TableCell', blocks: [ { - blockType: 'Paragraph', segments: [ { - segmentType: 'Text', text: 'ID', + segmentType: 'Text', format: { + textColor: 'black', fontFamily: 'Calibri, sans-serif', fontSize: '11pt', fontWeight: '700', - textColor: 'black', }, }, ], + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', + }, + blockType: 'Paragraph', format: { textAlign: 'center', whiteSpace: 'nowrap', @@ -137,30 +147,33 @@ describe(ID, () => { verticalAlign: 'middle', width: '56pt', }, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: { - ogsb: 'white', - }, + dataset: { ogsb: 'white' }, }, { + spanAbove: false, + spanLeft: false, + isHeader: false, blockGroupType: 'TableCell', blocks: [ { - blockType: 'Paragraph', segments: [ { - segmentType: 'Text', text: 'Work Item Type', + segmentType: 'Text', format: { + textColor: 'black', fontFamily: 'Calibri, sans-serif', fontSize: '11pt', fontWeight: '700', - textColor: 'black', }, }, ], + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '11pt', + textColor: 'black', + }, + blockType: 'Paragraph', format: { textAlign: 'center', marginTop: '0px', @@ -182,33 +195,28 @@ describe(ID, () => { verticalAlign: 'middle', width: '62pt', }, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: { - ogsb: 'white', - }, + dataset: { ogsb: 'white' }, }, ], + format: {}, }, ], - format: <any>{ + blockType: 'Table', + format: { textAlign: 'start', backgroundColor: 'rgb(255, 255, 255)', width: '170pt', + textColor: 'rgb(0, 0, 0)', useBorderBox: true, borderCollapse: true, - textColor: 'rgb(0, 0, 0)', - }, - widths: <any>jasmine.anything(), + } as any, dataset: {}, }, { - blockType: 'Paragraph', segments: [ { - segmentType: 'SelectionMarker', isSelected: true, + segmentType: 'SelectionMarker', format: { backgroundColor: '', fontFamily: '', @@ -223,11 +231,9 @@ describe(ID, () => { underline: false, }, }, - { - segmentType: 'Br', - format: {}, - }, + { segmentType: 'Br', format: {} }, ], + blockType: 'Paragraph', format: {}, }, ], diff --git a/packages/roosterjs-content-model-plugins/test/paste/e2e/htmlTemplates/wordClipboardContent.ts b/packages/roosterjs-content-model-plugins/test/paste/e2e/htmlTemplates/wordClipboardContent.ts index c96e3263ebdd..3a43bcf1a2ee 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/e2e/htmlTemplates/wordClipboardContent.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/e2e/htmlTemplates/wordClipboardContent.ts @@ -67,3 +67,37 @@ export const wordClipboardContent3: ClipboardData = { "\r\n\r\n<table class=MsoNormalTable border=1 cellspacing=0 cellpadding=0 align=left\r\n width=774 style='width:580.5pt;border-collapse:collapse;border:none;\r\n mso-border-alt:solid #A3A3A3 1.0pt;mso-yfti-tbllook:1184;mso-table-lspace:\r\n 9.0pt;margin-left:6.75pt;mso-table-rspace:9.0pt;margin-right:6.75pt;\r\n mso-table-anchor-vertical:margin;mso-table-anchor-horizontal:margin;\r\n mso-table-left:center;mso-table-top:59.25pt;mso-padding-alt:0cm 0cm 0cm 0cm'>■■■<tr style='mso-yfti-irow:0;mso-yfti-firstrow:yes;mso-yfti-lastrow:yes;\r\n height:194.05pt'>■■■■<td width=348 valign=top style='width:261.0pt;border:solid #A3A3A3 1.0pt;\r\n padding:4.0pt 4.0pt 4.0pt 4.0pt;height:194.05pt'>■■■■<ul style='margin-top:0cm' type=disc>■■■■■<li class=MsoNormal style='margin-bottom:0cm;line-height:normal;mso-list:\r\n l0 level1 lfo1;vertical-align:middle;mso-element:frame;mso-element-frame-hspace:\r\n 9.0pt;mso-element-wrap:around;mso-element-anchor-horizontal:margin;\r\n mso-element-left:center;mso-element-top:59.25pt;mso-height-rule:exactly'><span\r\n lang=EN-US style='mso-ascii-font-family:Calibri;mso-fareast-font-family:\r\n \"Times New Roman\";mso-hansi-font-family:Calibri;mso-bidi-font-family:\r\n Calibri;mso-fareast-language:EN-IN'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></li>■■■■■<li class=MsoNormal style='margin-bottom:0cm;line-height:normal;mso-list:\r\n l0 level1 lfo1;vertical-align:middle;mso-element:frame;mso-element-frame-hspace:\r\n 9.0pt;mso-element-wrap:around;mso-element-anchor-horizontal:margin;\r\n mso-element-left:center;mso-element-top:59.25pt;mso-height-rule:exactly'><span\r\n lang=EN-US style='mso-ascii-font-family:Calibri;mso-fareast-font-family:\r\n \"Times New Roman\";mso-hansi-font-family:Calibri;mso-bidi-font-family:\r\n Calibri;mso-fareast-language:EN-IN'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></li>■■■■■<li class=MsoNormal style='margin-bottom:0cm;line-height:normal;mso-list:\r\n l0 level1 lfo1;vertical-align:middle;mso-element:frame;mso-element-frame-hspace:\r\n 9.0pt;mso-element-wrap:around;mso-element-anchor-horizontal:margin;\r\n mso-element-left:center;mso-element-top:59.25pt;mso-height-rule:exactly'><span\r\n lang=EN-US style='mso-ascii-font-family:Calibri;mso-fareast-font-family:\r\n \"Times New Roman\";mso-hansi-font-family:Calibri;mso-bidi-font-family:\r\n Calibri;mso-fareast-language:EN-IN'>■■■■■■■■■■■■<o:p></o:p></span></li>■■■■■<ul style='margin-top:0cm' type=circle>■■■■■■<li class=MsoNormal style='margin-bottom:0cm;line-height:normal;mso-list:\r\n l0 level2 lfo1;vertical-align:middle;mso-element:frame;mso-element-frame-hspace:\r\n 9.0pt;mso-element-wrap:around;mso-element-anchor-horizontal:margin;\r\n mso-element-left:center;mso-element-top:59.25pt;mso-height-rule:exactly'><span\r\n lang=EN-US style='mso-ascii-font-family:Calibri;mso-fareast-font-family:\r\n \"Times New Roman\";mso-hansi-font-family:Calibri;mso-bidi-font-family:\r\n Calibri;mso-fareast-language:EN-IN'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></li>■■■■■■<li class=MsoNormal style='margin-bottom:0cm;line-height:normal;mso-list:\r\n l0 level2 lfo1;vertical-align:middle;mso-element:frame;mso-element-frame-hspace:\r\n 9.0pt;mso-element-wrap:around;mso-element-anchor-horizontal:margin;\r\n mso-element-left:center;mso-element-top:59.25pt;mso-height-rule:exactly'><span\r\n lang=EN-US style='mso-ascii-font-family:Calibri;mso-fareast-font-family:\r\n \"Times New Roman\";mso-hansi-font-family:Calibri;mso-bidi-font-family:\r\n Calibri;mso-fareast-language:EN-IN'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></li>■■■■■■<li class=MsoNormal style='margin-bottom:0cm;line-height:normal;mso-list:\r\n l0 level2 lfo1;vertical-align:middle;mso-element:frame;mso-element-frame-hspace:\r\n 9.0pt;mso-element-wrap:around;mso-element-anchor-horizontal:margin;\r\n mso-element-left:center;mso-element-top:59.25pt;mso-height-rule:exactly'><span\r\n lang=EN-US style='mso-ascii-font-family:Calibri;mso-fareast-font-family:\r\n \"Times New Roman\";mso-hansi-font-family:Calibri;mso-bidi-font-family:\r\n Calibri;mso-fareast-language:EN-IN'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></li>■■■■■</ul>■■■■</ul>■■■■</td>■■■</tr>■■</table>\r\n\r\n", htmlFirstLevelChildTags: ['TABLE'], }; + +export const wordClipboardContent4: ClipboardData = { + types: ['text/plain', 'text/html'], + text: + '■■■■■■■■■■■■■■■■■■■■■\r\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\r\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\r\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\r\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\r\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\r\n\r\n■■■■■■■■■■■■■■■■■■\r\n■■■■■■■■■■■■■■■■■■■■■■■■■■■\r\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\r\n■■■■■■■■■■■■\r\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\r\n\r\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\r\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\r\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\r\n\r\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\r\n', + image: null, + files: [], + rawHtml: + '<html xmlns:o="urn:schemas-microsoft-com:office:office"\r\nxmlns:w="urn:schemas-microsoft-com:office:word"\r\nxmlns:m="http://schemas.microsoft.com/office/2004/12/omml"\r\nxmlns="http://www.w3.org/TR/REC-html40">■■■■<head>■■<meta http-equiv=Content-Type content="text/html; charset=utf-8">■■<meta name=ProgId content=Word.Document>■■<meta name=Generator content="Microsoft Word 15">■■<meta name=Originator content="Microsoft Word 15">■■<link rel=File-List\r\n>■■<!--[if gte mso 9]><xml>■■■<o:OfficeDocumentSettings>■■■■<o:AllowPNG/>■■■</o:OfficeDocumentSettings>■■</xml><![endif]-->■■<link rel=themeData\r\n>■■<link rel=colorSchemeMapping\r\n>■■<!--[if gte mso 9]><xml>■■■<w:WordDocument>■■■■<w:View>■■■■■■</w:View>■■■■<w:Zoom>■</w:Zoom>■■■■<w:TrackMoves>■■■■■</w:TrackMoves>■■■■<w:TrackFormatting/>■■■■<w:PunctuationKerning/>■■■■<w:ValidateAgainstSchemas/>■■■■<w:SaveIfXMLInvalid>■■■■■</w:SaveIfXMLInvalid>■■■■<w:IgnoreMixedContent>■■■■■</w:IgnoreMixedContent>■■■■<w:AlwaysShowPlaceholderText>■■■■■</w:AlwaysShowPlaceholderText>■■■■<w:DoNotPromoteQF/>■■■■<w:LidThemeOther>■■■■■</w:LidThemeOther>■■■■<w:LidThemeAsian>■■</w:LidThemeAsian>■■■■<w:LidThemeComplexScript>■■■■■</w:LidThemeComplexScript>■■■■<w:Compatibility>■■■■■<w:BreakWrappedTables/>■■■■■<w:SnapToGridInCell/>■■■■■<w:WrapTextWithPunct/>■■■■■<w:UseAsianBreakRules/>■■■■■<w:DontGrowAutofit/>■■■■■<w:SplitPgBreakAndParaMark/>■■■■■<w:EnableOpenTypeKerning/>■■■■■<w:DontFlipMirrorIndents/>■■■■■<w:OverrideTableStyleHps/>■■■■</w:Compatibility>■■■■<m:mathPr>■■■■■<m:mathFont m:val="Cambria Math"/>■■■■■<m:brkBin m:val="before"/>■■■■■<m:brkBinSub m:val="--"/>■■■■■<m:smallFrac m:val="off"/>■■■■■<m:dispDef/>■■■■■<m:lMargin m:val="0"/>■■■■■<m:rMargin m:val="0"/>■■■■■<m:defJc m:val="centerGroup"/>■■■■■<m:wrapIndent m:val="1440"/>■■■■■<m:intLim m:val="subSup"/>■■■■■<m:naryLim m:val="undOvr"/>■■■■</m:mathPr></w:WordDocument>■■</xml><![endif]--><!--[if gte mso 9]><xml>■■■<w:LatentStyles DefLockedState="false" DefUnhideWhenUsed="false"\r\n DefSemiHidden="false" DefQFormat="false" DefPriority="99"\r\n LatentStyleCount="376">■■■■<w:LsdException Locked="false" Priority="0" QFormat="true" Name="Normal"/>■■■■<w:LsdException Locked="false" Priority="9" QFormat="true" Name="heading 1"/>■■■■<w:LsdException Locked="false" Priority="9" SemiHidden="true"\r\n UnhideWhenUsed="true" QFormat="true" Name="heading 2"/>■■■■<w:LsdException Locked="false" Priority="9" SemiHidden="true"\r\n UnhideWhenUsed="true" QFormat="true" Name="heading 3"/>■■■■<w:LsdException Locked="false" Priority="9" SemiHidden="true"\r\n UnhideWhenUsed="true" QFormat="true" Name="heading 4"/>■■■■<w:LsdException Locked="false" Priority="9" SemiHidden="true"\r\n UnhideWhenUsed="true" QFormat="true" Name="heading 5"/>■■■■<w:LsdException Locked="false" Priority="9" SemiHidden="true"\r\n UnhideWhenUsed="true" QFormat="true" Name="heading 6"/>■■■■<w:LsdException Locked="false" Priority="9" SemiHidden="true"\r\n UnhideWhenUsed="true" QFormat="true" Name="heading 7"/>■■■■<w:LsdException Locked="false" Priority="9" SemiHidden="true"\r\n UnhideWhenUsed="true" QFormat="true" Name="heading 8"/>■■■■<w:LsdException Locked="false" Priority="9" SemiHidden="true"\r\n UnhideWhenUsed="true" QFormat="true" Name="heading 9"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="index 1"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="index 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="index 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="index 4"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="index 5"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="index 6"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="index 7"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="index 8"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="index 9"/>■■■■<w:LsdException Locked="false" Priority="39" SemiHidden="true"\r\n UnhideWhenUsed="true" Name="toc 1"/>■■■■<w:LsdException Locked="false" Priority="39" SemiHidden="true"\r\n UnhideWhenUsed="true" Name="toc 2"/>■■■■<w:LsdException Locked="false" Priority="39" SemiHidden="true"\r\n UnhideWhenUsed="true" Name="toc 3"/>■■■■<w:LsdException Locked="false" Priority="39" SemiHidden="true"\r\n UnhideWhenUsed="true" Name="toc 4"/>■■■■<w:LsdException Locked="false" Priority="39" SemiHidden="true"\r\n UnhideWhenUsed="true" Name="toc 5"/>■■■■<w:LsdException Locked="false" Priority="39" SemiHidden="true"\r\n UnhideWhenUsed="true" Name="toc 6"/>■■■■<w:LsdException Locked="false" Priority="39" SemiHidden="true"\r\n UnhideWhenUsed="true" Name="toc 7"/>■■■■<w:LsdException Locked="false" Priority="39" SemiHidden="true"\r\n UnhideWhenUsed="true" Name="toc 8"/>■■■■<w:LsdException Locked="false" Priority="39" SemiHidden="true"\r\n UnhideWhenUsed="true" Name="toc 9"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Normal Indent"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="footnote text"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="annotation text"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="header"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="footer"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="index heading"/>■■■■<w:LsdException Locked="false" Priority="35" SemiHidden="true"\r\n UnhideWhenUsed="true" QFormat="true" Name="caption"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="table of figures"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="envelope address"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="envelope return"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="footnote reference"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="annotation reference"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="line number"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="page number"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="endnote reference"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="endnote text"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="table of authorities"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="macro"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="toa heading"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Bullet"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Number"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List 4"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List 5"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Bullet 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Bullet 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Bullet 4"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Bullet 5"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Number 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Number 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Number 4"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Number 5"/>■■■■<w:LsdException Locked="false" Priority="10" QFormat="true" Name="Title"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Closing"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Signature"/>■■■■<w:LsdException Locked="false" Priority="1" SemiHidden="true"\r\n UnhideWhenUsed="true" Name="Default Paragraph Font"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Body Text"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Body Text Indent"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Continue"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Continue 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Continue 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Continue 4"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="List Continue 5"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Message Header"/>■■■■<w:LsdException Locked="false" Priority="11" QFormat="true" Name="Subtitle"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Salutation"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Date"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Body Text First Indent"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Body Text First Indent 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Note Heading"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Body Text 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Body Text 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Body Text Indent 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Body Text Indent 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Block Text"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n QFormat="true" Name="Hyperlink"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="FollowedHyperlink"/>■■■■<w:LsdException Locked="false" Priority="22" QFormat="true" Name="Strong"/>■■■■<w:LsdException Locked="false" Priority="20" QFormat="true" Name="Emphasis"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Document Map"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Plain Text"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="E-mail Signature"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="HTML Top of Form"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="HTML Bottom of Form"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Normal (Web)"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="HTML Acronym"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="HTML Address"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="HTML Cite"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="HTML Code"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="HTML Definition"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="HTML Keyboard"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="HTML Preformatted"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="HTML Sample"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="HTML Typewriter"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="HTML Variable"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Normal Table"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="annotation subject"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="No List"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Outline List 1"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Outline List 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Outline List 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Simple 1"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Simple 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Simple 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Classic 1"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Classic 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Classic 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Classic 4"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Colorful 1"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Colorful 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Colorful 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Columns 1"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Columns 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Columns 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Columns 4"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Columns 5"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Grid 1"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Grid 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Grid 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Grid 4"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Grid 5"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Grid 6"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Grid 7"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Grid 8"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table List 1"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table List 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table List 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table List 4"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table List 5"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table List 6"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table List 7"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table List 8"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table 3D effects 1"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table 3D effects 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table 3D effects 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Contemporary"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Elegant"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Professional"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Subtle 1"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Subtle 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Web 1"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Web 2"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Web 3"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Balloon Text"/>■■■■<w:LsdException Locked="false" Priority="39" Name="Table Grid"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Table Theme"/>■■■■<w:LsdException Locked="false" SemiHidden="true" Name="Placeholder Text"/>■■■■<w:LsdException Locked="false" Priority="1" QFormat="true" Name="No Spacing"/>■■■■<w:LsdException Locked="false" Priority="60" Name="Light Shading"/>■■■■<w:LsdException Locked="false" Priority="61" Name="Light List"/>■■■■<w:LsdException Locked="false" Priority="62" Name="Light Grid"/>■■■■<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1"/>■■■■<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2"/>■■■■<w:LsdException Locked="false" Priority="65" Name="Medium List 1"/>■■■■<w:LsdException Locked="false" Priority="66" Name="Medium List 2"/>■■■■<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1"/>■■■■<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2"/>■■■■<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3"/>■■■■<w:LsdException Locked="false" Priority="70" Name="Dark List"/>■■■■<w:LsdException Locked="false" Priority="71" Name="Colorful Shading"/>■■■■<w:LsdException Locked="false" Priority="72" Name="Colorful List"/>■■■■<w:LsdException Locked="false" Priority="73" Name="Colorful Grid"/>■■■■<w:LsdException Locked="false" Priority="60" Name="Light Shading Accent 1"/>■■■■<w:LsdException Locked="false" Priority="61" Name="Light List Accent 1"/>■■■■<w:LsdException Locked="false" Priority="62" Name="Light Grid Accent 1"/>■■■■<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1 Accent 1"/>■■■■<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2 Accent 1"/>■■■■<w:LsdException Locked="false" Priority="65" Name="Medium List 1 Accent 1"/>■■■■<w:LsdException Locked="false" SemiHidden="true" Name="Revision"/>■■■■<w:LsdException Locked="false" Priority="34" QFormat="true"\r\n Name="List Paragraph"/>■■■■<w:LsdException Locked="false" Priority="29" QFormat="true" Name="Quote"/>■■■■<w:LsdException Locked="false" Priority="30" QFormat="true"\r\n Name="Intense Quote"/>■■■■<w:LsdException Locked="false" Priority="66" Name="Medium List 2 Accent 1"/>■■■■<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1 Accent 1"/>■■■■<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2 Accent 1"/>■■■■<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3 Accent 1"/>■■■■<w:LsdException Locked="false" Priority="70" Name="Dark List Accent 1"/>■■■■<w:LsdException Locked="false" Priority="71" Name="Colorful Shading Accent 1"/>■■■■<w:LsdException Locked="false" Priority="72" Name="Colorful List Accent 1"/>■■■■<w:LsdException Locked="false" Priority="73" Name="Colorful Grid Accent 1"/>■■■■<w:LsdException Locked="false" Priority="60" Name="Light Shading Accent 2"/>■■■■<w:LsdException Locked="false" Priority="61" Name="Light List Accent 2"/>■■■■<w:LsdException Locked="false" Priority="62" Name="Light Grid Accent 2"/>■■■■<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1 Accent 2"/>■■■■<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2 Accent 2"/>■■■■<w:LsdException Locked="false" Priority="65" Name="Medium List 1 Accent 2"/>■■■■<w:LsdException Locked="false" Priority="66" Name="Medium List 2 Accent 2"/>■■■■<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1 Accent 2"/>■■■■<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2 Accent 2"/>■■■■<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3 Accent 2"/>■■■■<w:LsdException Locked="false" Priority="70" Name="Dark List Accent 2"/>■■■■<w:LsdException Locked="false" Priority="71" Name="Colorful Shading Accent 2"/>■■■■<w:LsdException Locked="false" Priority="72" Name="Colorful List Accent 2"/>■■■■<w:LsdException Locked="false" Priority="73" Name="Colorful Grid Accent 2"/>■■■■<w:LsdException Locked="false" Priority="60" Name="Light Shading Accent 3"/>■■■■<w:LsdException Locked="false" Priority="61" Name="Light List Accent 3"/>■■■■<w:LsdException Locked="false" Priority="62" Name="Light Grid Accent 3"/>■■■■<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1 Accent 3"/>■■■■<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2 Accent 3"/>■■■■<w:LsdException Locked="false" Priority="65" Name="Medium List 1 Accent 3"/>■■■■<w:LsdException Locked="false" Priority="66" Name="Medium List 2 Accent 3"/>■■■■<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1 Accent 3"/>■■■■<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2 Accent 3"/>■■■■<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3 Accent 3"/>■■■■<w:LsdException Locked="false" Priority="70" Name="Dark List Accent 3"/>■■■■<w:LsdException Locked="false" Priority="71" Name="Colorful Shading Accent 3"/>■■■■<w:LsdException Locked="false" Priority="72" Name="Colorful List Accent 3"/>■■■■<w:LsdException Locked="false" Priority="73" Name="Colorful Grid Accent 3"/>■■■■<w:LsdException Locked="false" Priority="60" Name="Light Shading Accent 4"/>■■■■<w:LsdException Locked="false" Priority="61" Name="Light List Accent 4"/>■■■■<w:LsdException Locked="false" Priority="62" Name="Light Grid Accent 4"/>■■■■<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1 Accent 4"/>■■■■<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2 Accent 4"/>■■■■<w:LsdException Locked="false" Priority="65" Name="Medium List 1 Accent 4"/>■■■■<w:LsdException Locked="false" Priority="66" Name="Medium List 2 Accent 4"/>■■■■<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1 Accent 4"/>■■■■<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2 Accent 4"/>■■■■<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3 Accent 4"/>■■■■<w:LsdException Locked="false" Priority="70" Name="Dark List Accent 4"/>■■■■<w:LsdException Locked="false" Priority="71" Name="Colorful Shading Accent 4"/>■■■■<w:LsdException Locked="false" Priority="72" Name="Colorful List Accent 4"/>■■■■<w:LsdException Locked="false" Priority="73" Name="Colorful Grid Accent 4"/>■■■■<w:LsdException Locked="false" Priority="60" Name="Light Shading Accent 5"/>■■■■<w:LsdException Locked="false" Priority="61" Name="Light List Accent 5"/>■■■■<w:LsdException Locked="false" Priority="62" Name="Light Grid Accent 5"/>■■■■<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1 Accent 5"/>■■■■<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2 Accent 5"/>■■■■<w:LsdException Locked="false" Priority="65" Name="Medium List 1 Accent 5"/>■■■■<w:LsdException Locked="false" Priority="66" Name="Medium List 2 Accent 5"/>■■■■<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1 Accent 5"/>■■■■<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2 Accent 5"/>■■■■<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3 Accent 5"/>■■■■<w:LsdException Locked="false" Priority="70" Name="Dark List Accent 5"/>■■■■<w:LsdException Locked="false" Priority="71" Name="Colorful Shading Accent 5"/>■■■■<w:LsdException Locked="false" Priority="72" Name="Colorful List Accent 5"/>■■■■<w:LsdException Locked="false" Priority="73" Name="Colorful Grid Accent 5"/>■■■■<w:LsdException Locked="false" Priority="60" Name="Light Shading Accent 6"/>■■■■<w:LsdException Locked="false" Priority="61" Name="Light List Accent 6"/>■■■■<w:LsdException Locked="false" Priority="62" Name="Light Grid Accent 6"/>■■■■<w:LsdException Locked="false" Priority="63" Name="Medium Shading 1 Accent 6"/>■■■■<w:LsdException Locked="false" Priority="64" Name="Medium Shading 2 Accent 6"/>■■■■<w:LsdException Locked="false" Priority="65" Name="Medium List 1 Accent 6"/>■■■■<w:LsdException Locked="false" Priority="66" Name="Medium List 2 Accent 6"/>■■■■<w:LsdException Locked="false" Priority="67" Name="Medium Grid 1 Accent 6"/>■■■■<w:LsdException Locked="false" Priority="68" Name="Medium Grid 2 Accent 6"/>■■■■<w:LsdException Locked="false" Priority="69" Name="Medium Grid 3 Accent 6"/>■■■■<w:LsdException Locked="false" Priority="70" Name="Dark List Accent 6"/>■■■■<w:LsdException Locked="false" Priority="71" Name="Colorful Shading Accent 6"/>■■■■<w:LsdException Locked="false" Priority="72" Name="Colorful List Accent 6"/>■■■■<w:LsdException Locked="false" Priority="73" Name="Colorful Grid Accent 6"/>■■■■<w:LsdException Locked="false" Priority="19" QFormat="true"\r\n Name="Subtle Emphasis"/>■■■■<w:LsdException Locked="false" Priority="21" QFormat="true"\r\n Name="Intense Emphasis"/>■■■■<w:LsdException Locked="false" Priority="31" QFormat="true"\r\n Name="Subtle Reference"/>■■■■<w:LsdException Locked="false" Priority="32" QFormat="true"\r\n Name="Intense Reference"/>■■■■<w:LsdException Locked="false" Priority="33" QFormat="true" Name="Book Title"/>■■■■<w:LsdException Locked="false" Priority="37" SemiHidden="true"\r\n UnhideWhenUsed="true" Name="Bibliography"/>■■■■<w:LsdException Locked="false" Priority="39" SemiHidden="true"\r\n UnhideWhenUsed="true" QFormat="true" Name="TOC Heading"/>■■■■<w:LsdException Locked="false" Priority="41" Name="Plain Table 1"/>■■■■<w:LsdException Locked="false" Priority="42" Name="Plain Table 2"/>■■■■<w:LsdException Locked="false" Priority="43" Name="Plain Table 3"/>■■■■<w:LsdException Locked="false" Priority="44" Name="Plain Table 4"/>■■■■<w:LsdException Locked="false" Priority="45" Name="Plain Table 5"/>■■■■<w:LsdException Locked="false" Priority="40" Name="Grid Table Light"/>■■■■<w:LsdException Locked="false" Priority="46" Name="Grid Table 1 Light"/>■■■■<w:LsdException Locked="false" Priority="47" Name="Grid Table 2"/>■■■■<w:LsdException Locked="false" Priority="48" Name="Grid Table 3"/>■■■■<w:LsdException Locked="false" Priority="49" Name="Grid Table 4"/>■■■■<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark"/>■■■■<w:LsdException Locked="false" Priority="51" Name="Grid Table 6 Colorful"/>■■■■<w:LsdException Locked="false" Priority="52" Name="Grid Table 7 Colorful"/>■■■■<w:LsdException Locked="false" Priority="46"\r\n Name="Grid Table 1 Light Accent 1"/>■■■■<w:LsdException Locked="false" Priority="47" Name="Grid Table 2 Accent 1"/>■■■■<w:LsdException Locked="false" Priority="48" Name="Grid Table 3 Accent 1"/>■■■■<w:LsdException Locked="false" Priority="49" Name="Grid Table 4 Accent 1"/>■■■■<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark Accent 1"/>■■■■<w:LsdException Locked="false" Priority="51"\r\n Name="Grid Table 6 Colorful Accent 1"/>■■■■<w:LsdException Locked="false" Priority="52"\r\n Name="Grid Table 7 Colorful Accent 1"/>■■■■<w:LsdException Locked="false" Priority="46"\r\n Name="Grid Table 1 Light Accent 2"/>■■■■<w:LsdException Locked="false" Priority="47" Name="Grid Table 2 Accent 2"/>■■■■<w:LsdException Locked="false" Priority="48" Name="Grid Table 3 Accent 2"/>■■■■<w:LsdException Locked="false" Priority="49" Name="Grid Table 4 Accent 2"/>■■■■<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark Accent 2"/>■■■■<w:LsdException Locked="false" Priority="51"\r\n Name="Grid Table 6 Colorful Accent 2"/>■■■■<w:LsdException Locked="false" Priority="52"\r\n Name="Grid Table 7 Colorful Accent 2"/>■■■■<w:LsdException Locked="false" Priority="46"\r\n Name="Grid Table 1 Light Accent 3"/>■■■■<w:LsdException Locked="false" Priority="47" Name="Grid Table 2 Accent 3"/>■■■■<w:LsdException Locked="false" Priority="48" Name="Grid Table 3 Accent 3"/>■■■■<w:LsdException Locked="false" Priority="49" Name="Grid Table 4 Accent 3"/>■■■■<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark Accent 3"/>■■■■<w:LsdException Locked="false" Priority="51"\r\n Name="Grid Table 6 Colorful Accent 3"/>■■■■<w:LsdException Locked="false" Priority="52"\r\n Name="Grid Table 7 Colorful Accent 3"/>■■■■<w:LsdException Locked="false" Priority="46"\r\n Name="Grid Table 1 Light Accent 4"/>■■■■<w:LsdException Locked="false" Priority="47" Name="Grid Table 2 Accent 4"/>■■■■<w:LsdException Locked="false" Priority="48" Name="Grid Table 3 Accent 4"/>■■■■<w:LsdException Locked="false" Priority="49" Name="Grid Table 4 Accent 4"/>■■■■<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark Accent 4"/>■■■■<w:LsdException Locked="false" Priority="51"\r\n Name="Grid Table 6 Colorful Accent 4"/>■■■■<w:LsdException Locked="false" Priority="52"\r\n Name="Grid Table 7 Colorful Accent 4"/>■■■■<w:LsdException Locked="false" Priority="46"\r\n Name="Grid Table 1 Light Accent 5"/>■■■■<w:LsdException Locked="false" Priority="47" Name="Grid Table 2 Accent 5"/>■■■■<w:LsdException Locked="false" Priority="48" Name="Grid Table 3 Accent 5"/>■■■■<w:LsdException Locked="false" Priority="49" Name="Grid Table 4 Accent 5"/>■■■■<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark Accent 5"/>■■■■<w:LsdException Locked="false" Priority="51"\r\n Name="Grid Table 6 Colorful Accent 5"/>■■■■<w:LsdException Locked="false" Priority="52"\r\n Name="Grid Table 7 Colorful Accent 5"/>■■■■<w:LsdException Locked="false" Priority="46"\r\n Name="Grid Table 1 Light Accent 6"/>■■■■<w:LsdException Locked="false" Priority="47" Name="Grid Table 2 Accent 6"/>■■■■<w:LsdException Locked="false" Priority="48" Name="Grid Table 3 Accent 6"/>■■■■<w:LsdException Locked="false" Priority="49" Name="Grid Table 4 Accent 6"/>■■■■<w:LsdException Locked="false" Priority="50" Name="Grid Table 5 Dark Accent 6"/>■■■■<w:LsdException Locked="false" Priority="51"\r\n Name="Grid Table 6 Colorful Accent 6"/>■■■■<w:LsdException Locked="false" Priority="52"\r\n Name="Grid Table 7 Colorful Accent 6"/>■■■■<w:LsdException Locked="false" Priority="46" Name="List Table 1 Light"/>■■■■<w:LsdException Locked="false" Priority="47" Name="List Table 2"/>■■■■<w:LsdException Locked="false" Priority="48" Name="List Table 3"/>■■■■<w:LsdException Locked="false" Priority="49" Name="List Table 4"/>■■■■<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark"/>■■■■<w:LsdException Locked="false" Priority="51" Name="List Table 6 Colorful"/>■■■■<w:LsdException Locked="false" Priority="52" Name="List Table 7 Colorful"/>■■■■<w:LsdException Locked="false" Priority="46"\r\n Name="List Table 1 Light Accent 1"/>■■■■<w:LsdException Locked="false" Priority="47" Name="List Table 2 Accent 1"/>■■■■<w:LsdException Locked="false" Priority="48" Name="List Table 3 Accent 1"/>■■■■<w:LsdException Locked="false" Priority="49" Name="List Table 4 Accent 1"/>■■■■<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark Accent 1"/>■■■■<w:LsdException Locked="false" Priority="51"\r\n Name="List Table 6 Colorful Accent 1"/>■■■■<w:LsdException Locked="false" Priority="52"\r\n Name="List Table 7 Colorful Accent 1"/>■■■■<w:LsdException Locked="false" Priority="46"\r\n Name="List Table 1 Light Accent 2"/>■■■■<w:LsdException Locked="false" Priority="47" Name="List Table 2 Accent 2"/>■■■■<w:LsdException Locked="false" Priority="48" Name="List Table 3 Accent 2"/>■■■■<w:LsdException Locked="false" Priority="49" Name="List Table 4 Accent 2"/>■■■■<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark Accent 2"/>■■■■<w:LsdException Locked="false" Priority="51"\r\n Name="List Table 6 Colorful Accent 2"/>■■■■<w:LsdException Locked="false" Priority="52"\r\n Name="List Table 7 Colorful Accent 2"/>■■■■<w:LsdException Locked="false" Priority="46"\r\n Name="List Table 1 Light Accent 3"/>■■■■<w:LsdException Locked="false" Priority="47" Name="List Table 2 Accent 3"/>■■■■<w:LsdException Locked="false" Priority="48" Name="List Table 3 Accent 3"/>■■■■<w:LsdException Locked="false" Priority="49" Name="List Table 4 Accent 3"/>■■■■<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark Accent 3"/>■■■■<w:LsdException Locked="false" Priority="51"\r\n Name="List Table 6 Colorful Accent 3"/>■■■■<w:LsdException Locked="false" Priority="52"\r\n Name="List Table 7 Colorful Accent 3"/>■■■■<w:LsdException Locked="false" Priority="46"\r\n Name="List Table 1 Light Accent 4"/>■■■■<w:LsdException Locked="false" Priority="47" Name="List Table 2 Accent 4"/>■■■■<w:LsdException Locked="false" Priority="48" Name="List Table 3 Accent 4"/>■■■■<w:LsdException Locked="false" Priority="49" Name="List Table 4 Accent 4"/>■■■■<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark Accent 4"/>■■■■<w:LsdException Locked="false" Priority="51"\r\n Name="List Table 6 Colorful Accent 4"/>■■■■<w:LsdException Locked="false" Priority="52"\r\n Name="List Table 7 Colorful Accent 4"/>■■■■<w:LsdException Locked="false" Priority="46"\r\n Name="List Table 1 Light Accent 5"/>■■■■<w:LsdException Locked="false" Priority="47" Name="List Table 2 Accent 5"/>■■■■<w:LsdException Locked="false" Priority="48" Name="List Table 3 Accent 5"/>■■■■<w:LsdException Locked="false" Priority="49" Name="List Table 4 Accent 5"/>■■■■<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark Accent 5"/>■■■■<w:LsdException Locked="false" Priority="51"\r\n Name="List Table 6 Colorful Accent 5"/>■■■■<w:LsdException Locked="false" Priority="52"\r\n Name="List Table 7 Colorful Accent 5"/>■■■■<w:LsdException Locked="false" Priority="46"\r\n Name="List Table 1 Light Accent 6"/>■■■■<w:LsdException Locked="false" Priority="47" Name="List Table 2 Accent 6"/>■■■■<w:LsdException Locked="false" Priority="48" Name="List Table 3 Accent 6"/>■■■■<w:LsdException Locked="false" Priority="49" Name="List Table 4 Accent 6"/>■■■■<w:LsdException Locked="false" Priority="50" Name="List Table 5 Dark Accent 6"/>■■■■<w:LsdException Locked="false" Priority="51"\r\n Name="List Table 6 Colorful Accent 6"/>■■■■<w:LsdException Locked="false" Priority="52"\r\n Name="List Table 7 Colorful Accent 6"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Mention"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Smart Hyperlink"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Hashtag"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Unresolved Mention"/>■■■■<w:LsdException Locked="false" SemiHidden="true" UnhideWhenUsed="true"\r\n Name="Smart Link"/>■■■</w:LatentStyles>■■</xml><![endif]-->■■<style>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</style>■■<!--[if gte mso 10]>■■<style>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</style>■■<![endif]-->■■</head>■■■■<body lang=EN-US link="#0070C0" vlink="#96607D" style=\'tab-interval:.5in;\r\nword-wrap:break-word\'>■■<!--StartFragment-->■■■■<h1>■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></h1>■■■■<p class=MsoListParagraphCxSpFirst style=\'margin-bottom:2.0pt;mso-add-space:\r\nauto;text-indent:-.25in;mso-list:l0 level1 lfo3\'><![if !supportLists]><span\r\nstyle=\'mso-bidi-font-size:10.0pt;mso-fareast-font-family:Aptos;mso-fareast-theme-font:\r\nminor-latin;mso-bidi-font-family:Aptos;mso-bidi-theme-font:minor-latin;\r\nmso-fareast-language:EN-US\'><span style=\'mso-list:Ignore\'>■■<span\r\nstyle=\'font:7.0pt "Times New Roman"\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style=\'mso-bidi-font-size:10.0pt;\r\nmso-fareast-language:EN-US\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<p class=MsoListParagraphCxSpLast style=\'margin-bottom:2.0pt;mso-add-space:\r\nauto;text-indent:-.25in;mso-list:l0 level1 lfo3\'><![if !supportLists]><span\r\nstyle=\'mso-bidi-font-size:10.0pt;mso-fareast-font-family:Aptos;mso-fareast-theme-font:\r\nminor-latin;mso-bidi-font-family:Aptos;mso-bidi-theme-font:minor-latin;\r\nmso-fareast-language:EN-US\'><span style=\'mso-list:Ignore\'>■■<span\r\nstyle=\'font:7.0pt "Times New Roman"\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style=\'mso-bidi-font-size:10.0pt;\r\nmso-fareast-language:EN-US\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<h1>■■■■■■■■<a name="_Int_hqC0OfdX">■■■■■■■■■</a>■■■■■■■■■■■■■■■■■■■<o:p></o:p></h1>■■■■<p class=MsoListParagraphCxSpFirst style=\'margin-bottom:2.0pt;mso-add-space:\r\nauto;text-indent:-.25in;mso-list:l1 level1 lfo1\'><![if !supportLists]><span\r\nstyle=\'mso-bidi-font-size:10.0pt;mso-fareast-font-family:Aptos;mso-fareast-theme-font:\r\nminor-latin;mso-bidi-font-family:Aptos;mso-bidi-theme-font:minor-latin\'><span\r\nstyle=\'mso-list:Ignore\'>■■<span style=\'font:7.0pt "Times New Roman"\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style=\'mso-bidi-font-size:10.0pt\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<p class=MsoListParagraphCxSpLast style=\'margin-bottom:2.0pt;mso-add-space:\r\nauto;text-indent:-.25in;mso-list:l1 level1 lfo1\'><![if !supportLists]><span\r\nstyle=\'mso-bidi-font-size:10.0pt;mso-fareast-font-family:Aptos;mso-fareast-theme-font:\r\nminor-latin;mso-bidi-font-family:Aptos;mso-bidi-theme-font:minor-latin\'><span\r\nstyle=\'mso-list:Ignore\'>■■<span style=\'font:7.0pt "Times New Roman"\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style=\'mso-bidi-font-size:10.0pt\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<p class=MsoNormal><i><span style=\'mso-ascii-font-family:Aptos;mso-fareast-font-family:\r\nAptos;mso-hansi-font-family:Aptos;mso-bidi-font-family:Aptos;color:black;\r\nmso-themecolor:text1\'><o:p>■■■■■■</o:p></span></i></p>■■■■<p class=MsoNormal align=center style=\'margin-top:0in;margin-right:0in;\r\nmargin-bottom:20.0pt;margin-left:.25in;text-align:center\'><span\r\nstyle=\'font-family:"Aptos Light",sans-serif;mso-fareast-font-family:Aptos;\r\nmso-bidi-font-family:Aptos;color:#7F7F7F;mso-themecolor:text1;mso-themetint:\r\n128;mso-bidi-font-style:italic\'>■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<h1>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></h1>■■■■<p class=infopgh><span style=\'font-size:9.0pt;font-family:"Aptos",sans-serif;\r\ncolor:#252525;mso-themecolor:text1;mso-themetint:242;mso-style-textfill-fill-color:\r\n#252525;mso-style-textfill-fill-themecolor:text1;mso-style-textfill-fill-alpha:\r\n100.0%;mso-style-textfill-fill-colortransforms:"lumm=95000 lumo=5000 lumm=85000 lumo=15000 lumm=75000"\'>■■■■■■■■■■■■■■■■■■■■</span><a\r\n><span\r\nstyle=\'font-family:"Aptos",sans-serif;mso-ascii-theme-font:minor-latin;\r\nmso-fareast-font-family:"Yu Mincho";mso-fareast-theme-font:minor-fareast;\r\nmso-hansi-theme-font:minor-latin;mso-bidi-font-family:Arial;mso-bidi-theme-font:\r\nminor-bidi;color:#34ABFF;mso-style-textfill-fill-color:#34ABFF;mso-style-textfill-fill-alpha:\r\n100.0%;mso-fareast-language:JA\'>■■■</span></a><span style=\'font-size:9.0pt;\r\nfont-family:"Aptos",sans-serif;color:#252525;mso-themecolor:text1;mso-themetint:\r\n242;mso-style-textfill-fill-color:#252525;mso-style-textfill-fill-themecolor:\r\ntext1;mso-style-textfill-fill-alpha:100.0%;mso-style-textfill-fill-colortransforms:\r\n"lumm=95000 lumo=5000 lumm=85000 lumo=15000 lumm=75000"\'>■■</span><a\r\n><span\r\nstyle=\'font-family:"Aptos",sans-serif;mso-ascii-theme-font:minor-latin;\r\nmso-fareast-font-family:"Yu Mincho";mso-fareast-theme-font:minor-fareast;\r\nmso-hansi-theme-font:minor-latin;mso-bidi-font-family:Arial;mso-bidi-theme-font:\r\nminor-bidi;color:#34ABFF;mso-style-textfill-fill-color:#34ABFF;mso-style-textfill-fill-alpha:\r\n100.0%;mso-fareast-language:JA\'>■■■■■■■■■■■</span></a><span style=\'font-size:\r\n9.0pt;font-family:"Aptos",sans-serif\'>■■</span><a\r\n><span\r\nstyle=\'font-family:"Aptos",sans-serif;mso-ascii-theme-font:minor-latin;\r\nmso-fareast-font-family:"Yu Mincho";mso-fareast-theme-font:minor-fareast;\r\nmso-hansi-theme-font:minor-latin;mso-bidi-font-family:Arial;mso-bidi-theme-font:\r\nminor-bidi;color:#34ABFF;mso-style-textfill-fill-color:#34ABFF;mso-style-textfill-fill-alpha:\r\n100.0%;mso-fareast-language:JA\'>■■■■■■</span></a><i><span style=\'font-size:\r\n7.0pt\'><o:p></o:p></span></i></p>■■■■<h2>■■■■■■■■■■■■<o:p></o:p></h2>■■■■<p class=MsoNormal>■■■■■■■■■■<a\r\n>■■■■■■■■■■■■</a>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></p>■■■■<p class=MsoNormal><o:p>■■■■■■</o:p></p>■■■■<p class=MsoListParagraphCxSpFirst style=\'text-indent:-.25in;mso-list:l2 level1 lfo2\'><![if !supportLists]><span\r\nstyle=\'mso-bidi-font-size:10.0pt;mso-ascii-font-family:Aptos;mso-fareast-font-family:\r\nAptos;mso-hansi-font-family:Aptos;mso-bidi-font-family:Aptos\'><span\r\nstyle=\'mso-list:Ignore\'>■■<span style=\'font:7.0pt "Times New Roman"\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style=\'mso-bidi-font-size:10.0pt;\r\nmso-ascii-font-family:Aptos;mso-fareast-font-family:Aptos;mso-hansi-font-family:\r\nAptos;mso-bidi-font-family:Aptos\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<p class=MsoListParagraphCxSpMiddle style=\'text-indent:-.25in;mso-list:l2 level1 lfo2\'><![if !supportLists]><span\r\nstyle=\'mso-bidi-font-size:10.0pt;mso-ascii-font-family:Aptos;mso-fareast-font-family:\r\nAptos;mso-hansi-font-family:Aptos;mso-bidi-font-family:Aptos\'><span\r\nstyle=\'mso-list:Ignore\'>■■<span style=\'font:7.0pt "Times New Roman"\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style=\'mso-bidi-font-size:10.0pt;\r\nmso-ascii-font-family:Aptos;mso-fareast-font-family:Aptos;mso-hansi-font-family:\r\nAptos;mso-bidi-font-family:Aptos\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<p class=MsoListParagraphCxSpLast style=\'text-indent:-.25in;mso-list:l2 level1 lfo2\'><![if !supportLists]><span\r\nstyle=\'mso-bidi-font-size:10.0pt;mso-ascii-font-family:Aptos;mso-fareast-font-family:\r\nAptos;mso-hansi-font-family:Aptos;mso-bidi-font-family:Aptos\'><span\r\nstyle=\'mso-list:Ignore\'>■■<span style=\'font:7.0pt "Times New Roman"\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style=\'mso-bidi-font-size:10.0pt;\r\nmso-ascii-font-family:Aptos;mso-fareast-font-family:Aptos;mso-hansi-font-family:\r\nAptos;mso-bidi-font-family:Aptos\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<p class=MsoNormal><span style=\'mso-bidi-font-size:10.0pt;mso-ascii-font-family:\r\nAptos;mso-fareast-font-family:Aptos;mso-hansi-font-family:Aptos;mso-bidi-font-family:\r\nAptos\'><o:p>■■■■■■</o:p></span></p>■■■■<p class=MsoNormal><span style=\'mso-bidi-font-size:10.0pt;mso-ascii-font-family:\r\nAptos;mso-fareast-font-family:Aptos;mso-hansi-font-family:Aptos;mso-bidi-font-family:\r\nAptos\'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<!--EndFragment-->■■</body>■■■■</html>\r\n', + customValues: {}, + pasteNativeEvent: true, + html: + "\r\n\r\n<h1>■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></h1>■■■■<p class=MsoListParagraphCxSpFirst style='margin-bottom:2.0pt;mso-add-space:\r\nauto;text-indent:-.25in;mso-list:l0 level1 lfo3'><![if !supportLists]><span\r\nstyle='mso-bidi-font-size:10.0pt;mso-fareast-font-family:Aptos;mso-fareast-theme-font:\r\nminor-latin;mso-bidi-font-family:Aptos;mso-bidi-theme-font:minor-latin;\r\nmso-fareast-language:EN-US'><span style='mso-list:Ignore'>■■<span\r\nstyle='font:7.0pt \"Times New Roman\"'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style='mso-bidi-font-size:10.0pt;\r\nmso-fareast-language:EN-US'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<p class=MsoListParagraphCxSpLast style='margin-bottom:2.0pt;mso-add-space:\r\nauto;text-indent:-.25in;mso-list:l0 level1 lfo3'><![if !supportLists]><span\r\nstyle='mso-bidi-font-size:10.0pt;mso-fareast-font-family:Aptos;mso-fareast-theme-font:\r\nminor-latin;mso-bidi-font-family:Aptos;mso-bidi-theme-font:minor-latin;\r\nmso-fareast-language:EN-US'><span style='mso-list:Ignore'>■■<span\r\nstyle='font:7.0pt \"Times New Roman\"'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style='mso-bidi-font-size:10.0pt;\r\nmso-fareast-language:EN-US'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<h1>■■■■■■■■<a name=\"_Int_hqC0OfdX\">■■■■■■■■■</a>■■■■■■■■■■■■■■■■■■■<o:p></o:p></h1>■■■■<p class=MsoListParagraphCxSpFirst style='margin-bottom:2.0pt;mso-add-space:\r\nauto;text-indent:-.25in;mso-list:l1 level1 lfo1'><![if !supportLists]><span\r\nstyle='mso-bidi-font-size:10.0pt;mso-fareast-font-family:Aptos;mso-fareast-theme-font:\r\nminor-latin;mso-bidi-font-family:Aptos;mso-bidi-theme-font:minor-latin'><span\r\nstyle='mso-list:Ignore'>■■<span style='font:7.0pt \"Times New Roman\"'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style='mso-bidi-font-size:10.0pt'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<p class=MsoListParagraphCxSpLast style='margin-bottom:2.0pt;mso-add-space:\r\nauto;text-indent:-.25in;mso-list:l1 level1 lfo1'><![if !supportLists]><span\r\nstyle='mso-bidi-font-size:10.0pt;mso-fareast-font-family:Aptos;mso-fareast-theme-font:\r\nminor-latin;mso-bidi-font-family:Aptos;mso-bidi-theme-font:minor-latin'><span\r\nstyle='mso-list:Ignore'>■■<span style='font:7.0pt \"Times New Roman\"'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style='mso-bidi-font-size:10.0pt'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<p class=MsoNormal><i><span style='mso-ascii-font-family:Aptos;mso-fareast-font-family:\r\nAptos;mso-hansi-font-family:Aptos;mso-bidi-font-family:Aptos;color:black;\r\nmso-themecolor:text1'><o:p>■■■■■■</o:p></span></i></p>■■■■<p class=MsoNormal align=center style='margin-top:0in;margin-right:0in;\r\nmargin-bottom:20.0pt;margin-left:.25in;text-align:center'><span\r\nstyle='font-family:\"Aptos Light\",sans-serif;mso-fareast-font-family:Aptos;\r\nmso-bidi-font-family:Aptos;color:#7F7F7F;mso-themecolor:text1;mso-themetint:\r\n128;mso-bidi-font-style:italic'>■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<h1>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></h1>■■■■<p class=infopgh><span style='font-size:9.0pt;font-family:\"Aptos\",sans-serif;\r\ncolor:#252525;mso-themecolor:text1;mso-themetint:242;mso-style-textfill-fill-color:\r\n#252525;mso-style-textfill-fill-themecolor:text1;mso-style-textfill-fill-alpha:\r\n100.0%;mso-style-textfill-fill-colortransforms:\"lumm=95000 lumo=5000 lumm=85000 lumo=15000 lumm=75000\"'>■■■■■■■■■■■■■■■■■■■■</span><a\r\n><span\r\nstyle='font-family:\"Aptos\",sans-serif;mso-ascii-theme-font:minor-latin;\r\nmso-fareast-font-family:\"Yu Mincho\";mso-fareast-theme-font:minor-fareast;\r\nmso-hansi-theme-font:minor-latin;mso-bidi-font-family:Arial;mso-bidi-theme-font:\r\nminor-bidi;color:#34ABFF;mso-style-textfill-fill-color:#34ABFF;mso-style-textfill-fill-alpha:\r\n100.0%;mso-fareast-language:JA'>■■■</span></a><span style='font-size:9.0pt;\r\nfont-family:\"Aptos\",sans-serif;color:#252525;mso-themecolor:text1;mso-themetint:\r\n242;mso-style-textfill-fill-color:#252525;mso-style-textfill-fill-themecolor:\r\ntext1;mso-style-textfill-fill-alpha:100.0%;mso-style-textfill-fill-colortransforms:\r\n\"lumm=95000 lumo=5000 lumm=85000 lumo=15000 lumm=75000\"'>■■</span><a\r\n><span\r\nstyle='font-family:\"Aptos\",sans-serif;mso-ascii-theme-font:minor-latin;\r\nmso-fareast-font-family:\"Yu Mincho\";mso-fareast-theme-font:minor-fareast;\r\nmso-hansi-theme-font:minor-latin;mso-bidi-font-family:Arial;mso-bidi-theme-font:\r\nminor-bidi;color:#34ABFF;mso-style-textfill-fill-color:#34ABFF;mso-style-textfill-fill-alpha:\r\n100.0%;mso-fareast-language:JA'>■■■■■■■■■■■</span></a><span style='font-size:\r\n9.0pt;font-family:\"Aptos\",sans-serif'>■■</span><a\r\n><span\r\nstyle='font-family:\"Aptos\",sans-serif;mso-ascii-theme-font:minor-latin;\r\nmso-fareast-font-family:\"Yu Mincho\";mso-fareast-theme-font:minor-fareast;\r\nmso-hansi-theme-font:minor-latin;mso-bidi-font-family:Arial;mso-bidi-theme-font:\r\nminor-bidi;color:#34ABFF;mso-style-textfill-fill-color:#34ABFF;mso-style-textfill-fill-alpha:\r\n100.0%;mso-fareast-language:JA'>■■■■■■</span></a><i><span style='font-size:\r\n7.0pt'><o:p></o:p></span></i></p>■■■■<h2>■■■■■■■■■■■■<o:p></o:p></h2>■■■■<p class=MsoNormal>■■■■■■■■■■<a\r\n>■■■■■■■■■■■■</a>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></p>■■■■<p class=MsoNormal><o:p>■■■■■■</o:p></p>■■■■<p class=MsoListParagraphCxSpFirst style='text-indent:-.25in;mso-list:l2 level1 lfo2'><![if !supportLists]><span\r\nstyle='mso-bidi-font-size:10.0pt;mso-ascii-font-family:Aptos;mso-fareast-font-family:\r\nAptos;mso-hansi-font-family:Aptos;mso-bidi-font-family:Aptos'><span\r\nstyle='mso-list:Ignore'>■■<span style='font:7.0pt \"Times New Roman\"'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style='mso-bidi-font-size:10.0pt;\r\nmso-ascii-font-family:Aptos;mso-fareast-font-family:Aptos;mso-hansi-font-family:\r\nAptos;mso-bidi-font-family:Aptos'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<p class=MsoListParagraphCxSpMiddle style='text-indent:-.25in;mso-list:l2 level1 lfo2'><![if !supportLists]><span\r\nstyle='mso-bidi-font-size:10.0pt;mso-ascii-font-family:Aptos;mso-fareast-font-family:\r\nAptos;mso-hansi-font-family:Aptos;mso-bidi-font-family:Aptos'><span\r\nstyle='mso-list:Ignore'>■■<span style='font:7.0pt \"Times New Roman\"'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style='mso-bidi-font-size:10.0pt;\r\nmso-ascii-font-family:Aptos;mso-fareast-font-family:Aptos;mso-hansi-font-family:\r\nAptos;mso-bidi-font-family:Aptos'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<p class=MsoListParagraphCxSpLast style='text-indent:-.25in;mso-list:l2 level1 lfo2'><![if !supportLists]><span\r\nstyle='mso-bidi-font-size:10.0pt;mso-ascii-font-family:Aptos;mso-fareast-font-family:\r\nAptos;mso-hansi-font-family:Aptos;mso-bidi-font-family:Aptos'><span\r\nstyle='mso-list:Ignore'>■■<span style='font:7.0pt \"Times New Roman\"'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■</span></span></span><![endif]><span style='mso-bidi-font-size:10.0pt;\r\nmso-ascii-font-family:Aptos;mso-fareast-font-family:Aptos;mso-hansi-font-family:\r\nAptos;mso-bidi-font-family:Aptos'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>■■■■<p class=MsoNormal><span style='mso-bidi-font-size:10.0pt;mso-ascii-font-family:\r\nAptos;mso-fareast-font-family:Aptos;mso-hansi-font-family:Aptos;mso-bidi-font-family:\r\nAptos'><o:p>■■■■■■</o:p></span></p>■■■■<p class=MsoNormal><span style='mso-bidi-font-size:10.0pt;mso-ascii-font-family:\r\nAptos;mso-fareast-font-family:Aptos;mso-hansi-font-family:Aptos;mso-bidi-font-family:\r\nAptos'>■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■<o:p></o:p></span></p>\r\n\r\n", + htmlFirstLevelChildTags: [ + 'H1', + 'P', + 'P', + 'H1', + 'P', + 'P', + 'P', + 'P', + 'H1', + 'P', + 'H2', + 'P', + 'P', + 'P', + 'P', + 'P', + 'P', + 'P', + ], +}; diff --git a/packages/roosterjs-content-model-plugins/test/paste/plugin/ContentModelPastePluginTest.ts b/packages/roosterjs-content-model-plugins/test/paste/plugin/ContentModelPastePluginTest.ts index 6899b8181e57..b3dd98241be7 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/plugin/ContentModelPastePluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/plugin/ContentModelPastePluginTest.ts @@ -49,6 +49,7 @@ describe('Content Model Paste Plugin Test', () => { htmlAttributes: {}, pasteType: 'normal', domToModelOption: createDefaultDomToModelContext(), + globalCssRules: [], }; }); diff --git a/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWacTest.ts b/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWacTest.ts index d254b7198d6a..591c151ee483 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWacTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWacTest.ts @@ -2944,7 +2944,7 @@ describe('wordOnlineHandler', () => { describe('Contain Word WAC Image', () => { itChromeOnly('Contain Single WAC Image', () => { - runTest( + const [,] = runTest( '<span style="padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: relative; cursor: move; left: 0px; top: 2px; text-indent: 0px; color: rgb(0, 0, 0); font-family: "Segoe UI", "Segoe UI Web", Arial, Verdana, sans-serif; font-size: 12px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; width: auto; height: auto; transform: rotate(0deg);" role="presentation" class="WACImageContainer NoPadding DragDrop BlobObject SCXW139784418 BCX8"><img src="http://www.microsoft.com" style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; border: none; white-space: pre !important; vertical-align: baseline; width: 264px; height: 96px;" alt="Graphical user interface, text, application Description automatically generated" class="WACImage SCXW139784418 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; white-space: pre !important; display: block; position: absolute; transform: rotate(0deg); width: 264px; height: 96px; left: 0px; top: 0px;" class="WACImageBorder SCXW139784418 BCX8"></span></span>', undefined, { @@ -2969,10 +2969,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginBottom: '0px', marginLeft: '0px', - borderTop: '', - borderRight: '', - borderBottom: '', - borderLeft: '', verticalAlign: 'top', }, dataset: {}, @@ -2982,6 +2978,12 @@ describe('wordOnlineHandler', () => { ], format: {}, isImplicit: true, + segmentFormat: { + fontFamily: + '"Segoe UI", "Segoe UI Web", Arial, Verdana, sans-serif', + fontSize: '12px', + textColor: 'rgb(0, 0, 0)', + }, }, ], } @@ -3405,7 +3407,7 @@ describe('wordOnlineHandler', () => { blockType: 'Table', rows: [ { - height: 0, + height: 87, format: {}, cells: [ { @@ -3500,12 +3502,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginLeft: '0px', marginRight: '0px', whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', - textIndent: '0px', }, segmentFormat: { italic: false, @@ -3521,33 +3523,31 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginTop: '0px', marginRight: '0px', marginBottom: '0px', marginLeft: '0px', paddingRight: '6px', paddingLeft: '6px', - textIndent: '0px', }, }, ], format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', borderTop: '1px solid', borderBottom: '1px solid rgb(0, 0, 0)', borderLeft: '1px solid', backgroundColor: 'rgb(21, 96, 130)', verticalAlign: 'middle', width: '312px', - textIndent: '0px', }, spanLeft: false, spanAbove: false, isHeader: false, - dataset: { - celllook: '69905', - }, + dataset: { celllook: '69905' }, }, { blockGroupType: 'TableCell', @@ -3596,12 +3596,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginLeft: '0px', marginRight: '0px', whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', - textIndent: '0px', }, segmentFormat: { italic: false, @@ -3617,38 +3617,36 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginTop: '0px', marginRight: '0px', marginBottom: '0px', marginLeft: '0px', paddingRight: '6px', paddingLeft: '6px', - textIndent: '0px', }, }, ], format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', borderTop: '1px solid', borderRight: '1px solid', borderBottom: '1px solid rgb(0, 0, 0)', backgroundColor: 'rgb(21, 96, 130)', verticalAlign: 'middle', width: '312px', - textIndent: '0px', }, spanLeft: false, spanAbove: false, isHeader: false, - dataset: { - celllook: '69905', - }, + dataset: { celllook: '69905' }, }, ], }, { - height: 0, + height: 27, format: {}, cells: [ { @@ -3697,12 +3695,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginLeft: '0px', marginRight: '0px', whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', - textIndent: '0px', }, segmentFormat: { italic: false, @@ -3718,19 +3716,20 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginTop: '0px', marginRight: '0px', marginBottom: '0px', marginLeft: '0px', paddingRight: '6px', paddingLeft: '6px', - textIndent: '0px', }, }, ], format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', borderTop: '1px solid rgb(0, 0, 0)', borderRight: '1px solid', borderBottom: '1px solid rgb(0, 0, 0)', @@ -3738,14 +3737,11 @@ describe('wordOnlineHandler', () => { backgroundColor: 'rgb(0, 0, 0)', verticalAlign: 'middle', width: '624px', - textIndent: '0px', }, spanLeft: false, spanAbove: false, isHeader: false, - dataset: { - celllook: '69905', - }, + dataset: { celllook: '69905' }, }, { blockGroupType: 'TableCell', @@ -3753,6 +3749,7 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', borderTop: '1px solid rgb(0, 0, 0)', borderRight: '1px solid', borderBottom: '1px solid rgb(0, 0, 0)', @@ -3760,19 +3757,16 @@ describe('wordOnlineHandler', () => { backgroundColor: 'rgb(0, 0, 0)', verticalAlign: 'middle', width: '624px', - textIndent: '0px', }, spanLeft: true, spanAbove: false, isHeader: false, - dataset: { - celllook: '69905', - }, + dataset: { celllook: '69905' }, }, ], }, { - height: 0, + height: 20, format: {}, cells: [ { @@ -3822,12 +3816,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginLeft: '0px', marginRight: '0px', whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', - textIndent: '0px', }, segmentFormat: { italic: false, @@ -3862,12 +3856,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginLeft: '0px', marginRight: '0px', whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', - textIndent: '0px', }, segmentFormat: { italic: false, @@ -3950,12 +3944,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginLeft: '0px', marginRight: '0px', whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', - textIndent: '0px', }, segmentFormat: { italic: false, @@ -4071,12 +4065,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginLeft: '0px', marginRight: '0px', whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', - textIndent: '0px', }, segmentFormat: { italic: false, @@ -4110,12 +4104,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginLeft: '0px', marginRight: '0px', whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', - textIndent: '0px', }, segmentFormat: { italic: false, @@ -4165,12 +4159,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginLeft: '0px', marginRight: '0px', whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', - textIndent: '0px', }, segmentFormat: { italic: false, @@ -4204,12 +4198,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginLeft: '0px', marginRight: '0px', whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', - textIndent: '0px', }, segmentFormat: { italic: false, @@ -4303,12 +4297,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginLeft: '0px', marginRight: '0px', whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', - textIndent: '0px', }, segmentFormat: { italic: false, @@ -4391,12 +4385,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginLeft: '0px', marginRight: '0px', whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', - textIndent: '0px', }, segmentFormat: { italic: false, @@ -4412,33 +4406,31 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginTop: '0px', marginRight: '0px', marginBottom: '0px', marginLeft: '0px', paddingRight: '6px', paddingLeft: '6px', - textIndent: '0px', }, }, ], format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', borderTop: '1px solid rgb(0, 0, 0)', borderRight: '1px solid rgb(0, 0, 0)', borderBottom: '1px solid rgb(0, 0, 0)', borderLeft: '1px solid rgb(0, 0, 0)', verticalAlign: 'top', width: '624px', - textIndent: '0px', }, spanLeft: false, spanAbove: false, isHeader: false, - dataset: { - celllook: '4369', - }, + dataset: { celllook: '4369' }, }, { blockGroupType: 'TableCell', @@ -4446,20 +4438,18 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', borderTop: '1px solid rgb(0, 0, 0)', borderRight: '1px solid rgb(0, 0, 0)', borderBottom: '1px solid rgb(0, 0, 0)', borderLeft: '1px solid rgb(0, 0, 0)', verticalAlign: 'top', width: '624px', - textIndent: '0px', }, spanLeft: true, spanAbove: false, isHeader: false, - dataset: { - celllook: '4369', - }, + dataset: { celllook: '4369' }, }, ], }, @@ -4467,6 +4457,7 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginTop: '0px', marginRight: '0px', marginBottom: '0px', @@ -4474,39 +4465,35 @@ describe('wordOnlineHandler', () => { width: '0px', tableLayout: 'fixed', borderCollapse: true, - textIndent: '0px', + legacyTableBorder: '1', }, widths: [], - dataset: { - tablelook: '1696', - tablestyle: 'MsoTableGrid', - }, + dataset: { tablelook: '1696', tablestyle: 'MsoTableGrid' }, }, ], format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', marginTop: '2px', marginRight: '0px', marginBottom: '2px', - textIndent: '0px', }, }, ], format: { direction: 'ltr', textAlign: 'start', + textIndent: '0px', backgroundColor: 'rgb(255, 255, 255)', marginTop: '0px', marginRight: '0px', marginBottom: '0px', marginLeft: '0px', - textIndent: '0px', }, }, ], }; - runTest( '<div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; cursor: text; clear: both; position: relative; direction: ltr; color: rgb(0, 0, 0); font-family: "Segoe UI", "Segoe UI Web", Arial, Verdana, sans-serif; font-size: 12px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" class="OutlineElement Ltr BCX8 SCXW253660117"><div style="margin: 2px 0px 2px -5px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; position: relative; display: flex; justify-content: flex-start;" class="TableContainer Ltr BCX8 SCXW253660117"><table style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; table-layout: fixed; width: 0px; overflow: visible; border-collapse: collapse; empty-cells: show; position: relative; background: transparent; border-spacing: 0px;" aria-rowcount="3" data-tablelook="1696" data-tablestyle="MsoTableGrid" border="1" class="Table Ltr TableWordWrap BCX8 SCXW253660117"><tbody style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="BCX8 SCXW253660117"><tr style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; height: 87px;" aria-rowindex="1" role="row" class="TableRow BCX8 SCXW253660117"><td style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; vertical-align: middle; overflow: visible; position: relative; background-color: rgb(21, 96, 130); width: 312px; border-width: 1px 0px 1px 1px; border-style: solid none solid solid; border-top-color: initial; border-right-color: initial; border-bottom-color: rgb(0, 0, 0); border-left-color: initial; border-image: initial;" data-celllook="69905" role="rowheader" class="TableCellBorderSelection FirstRow FirstCol LowContrastShading BCX8 AdvancedProofingDarkMode ContextualSpellingDarkMode SpellingErrorWhite SimilarityReviewedWhite SimilarityUnreviewedWhite SCXW253660117"><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; display: flex; overflow: visible; width: 15px; height: 10px; cursor: pointer; z-index: 5; background-color: transparent; bottom: -5.5px; left: -15.5px;" class="TableHoverRowHandle BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; width: 20px; height: 12px; top: -12.5px; left: auto; cursor: pointer; z-index: 4; text-align: center; display: inline-block; background-color: transparent; right: -10px;" class="TableHoverColumnHandle BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; width: 7px; z-index: 1; background-repeat: repeat; cursor: pointer; height: calc(100% + 1px); left: -4px; top: -0.5px;" class="TableCellLeftBorderHandle BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; height: 7px; z-index: 1; background-repeat: repeat; background-color: transparent; cursor: pointer; left: -0.5px; width: calc(100% + 0.5px); top: -4px;" class="TableCellTopBorderHandle BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; bottom: 0px; overflow: visible; width: 7px; top: -0.5px; z-index: 3; background-repeat: repeat; height: calc(100% + 1px); cursor: url("https://res-1-sdf.cdn.office.net:443/officeonline/we/s/hD55E5E9C2AD2E4F5_resources/1033/ColResize.cur"), pointer; right: -3.5px;" class="TableColumnResizeHandle BCX8 SCXW253660117"></div><div style="margin: -3px 0px 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; height: 7px; z-index: 3; background-repeat: repeat; background-color: transparent; left: -0.5px; width: calc(100% + 0.5px); cursor: url("https://res-1-sdf.cdn.office.net:443/officeonline/we/s/h1E5273DBAA04AEF6_resources/1033/RowResize.cur"), pointer; bottom: -4px;" class="TableInsertRowGapBlank BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px 6px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible;" class="TableCellContent BCX8 SCXW253660117"><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; cursor: text; clear: both; position: relative; direction: ltr;" class="OutlineElement Ltr BCX8 SCXW253660117"><p style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;" class="Paragraph SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(255, 255, 255); font-size: 20pt; line-height: 41.85px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; font-weight: bold;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun BookmarkStart SCXW253660117 BCX8">ODSP</span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 20pt; line-height: 41.85px; font-family: WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(255, 255, 255);" class="LineBreakBlob BlobObject DragDrop SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; white-space: pre !important;" class="SCXW253660117 BCX8"> </span><br style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; white-space: pre !important;" class="SCXW253660117 BCX8"></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(255, 255, 255); font-size: 20pt; line-height: 41.85px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; font-weight: bold;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8">xFun</span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 20pt; line-height: 41.85px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(255, 255, 255);" data-ccp-props="{"201341983":0,"335559740":279}" class="EOP SCXW253660117 BCX8"> </span></p></div></div></td><td style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; vertical-align: middle; overflow: visible; position: relative; background-color: rgb(21, 96, 130); width: 312px; border-width: 1px 1px 1px 0px; border-style: solid solid solid none; border-top-color: initial; border-right-color: initial; border-bottom-color: rgb(0, 0, 0); border-left-color: initial; border-image: initial;" data-celllook="69905" role="columnheader" class="TableCellBorderSelection FirstRow LastCol LowContrastShading BCX8 AdvancedProofingDarkMode ContextualSpellingDarkMode SpellingErrorWhite SimilarityReviewedWhite SimilarityUnreviewedWhite SCXW253660117"><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; width: 20px; height: 12px; top: -12.5px; left: auto; cursor: pointer; z-index: 4; text-align: center; display: inline-block; background-color: transparent; right: -10.5px;" class="TableHoverColumnHandle BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; width: 7px; z-index: 1; background-repeat: repeat; cursor: pointer; height: calc(100% + 1px); left: -3.5px; top: -0.5px;" class="TableCellLeftBorderHandle BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; height: 7px; z-index: 1; background-repeat: repeat; background-color: transparent; cursor: pointer; left: 0px; width: calc(100% + 0.5px); top: -4px;" class="TableCellTopBorderHandle BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; bottom: 0px; overflow: visible; width: 7px; top: -0.5px; z-index: 3; background-repeat: repeat; height: calc(100% + 1px); cursor: url("https://res-1-sdf.cdn.office.net:443/officeonline/we/s/hD55E5E9C2AD2E4F5_resources/1033/ColResize.cur"), pointer; right: -4px;" class="TableColumnResizeHandle BCX8 SCXW253660117"></div><div style="margin: -3px 0px 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; height: 7px; z-index: 3; background-repeat: repeat; background-color: transparent; left: 0px; width: calc(100% + 0.5px); cursor: url("https://res-1-sdf.cdn.office.net:443/officeonline/we/s/h1E5273DBAA04AEF6_resources/1033/RowResize.cur"), pointer; bottom: -4px;" class="TableInsertRowGapBlank BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px 6px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible;" class="TableCellContent BCX8 SCXW253660117"><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; cursor: text; clear: both; position: relative; direction: ltr;" class="OutlineElement Ltr BCX8 SCXW253660117"><p style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;" class="Paragraph SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(255, 255, 255); font-size: 21.5pt; line-height: 44.175px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; font-weight: bold;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8">Title of Announcement</span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 21.5pt; line-height: 44.175px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(255, 255, 255);" data-ccp-props="{"201341983":0,"335559740":279}" class="EOP SCXW253660117 BCX8"> </span></p></div></div></td></tr><tr style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; height: 27px;" aria-rowindex="2" role="row" class="TableRow BCX8 SCXW253660117"><td style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; vertical-align: middle; overflow: visible; position: relative; background-color: rgb(0, 0, 0); width: 624px; border-width: 1px; border-style: solid; border-top-color: rgb(0, 0, 0); border-right-color: initial; border-bottom-color: rgb(0, 0, 0); border-left-color: initial; border-image: initial;" colspan="2" data-celllook="69905" role="rowheader" class="TableCellBorderSelection FirstCol LastCol LowContrastShading BCX8 AdvancedProofingDarkMode ContextualSpellingDarkMode SpellingErrorDarkMode SimilarityReviewedLightMode SimilarityUnreviewedDarkMode SCXW253660117"><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; display: flex; overflow: visible; width: 15px; height: 10px; cursor: pointer; z-index: 5; background-color: transparent; bottom: -5.5px; left: -15.5px;" class="TableHoverRowHandle BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; width: 7px; z-index: 1; background-repeat: repeat; cursor: pointer; height: calc(100% + 1px); left: -4px; top: -0.5px;" class="TableCellLeftBorderHandle BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; height: 7px; z-index: 1; background-repeat: repeat; background-color: transparent; cursor: pointer; left: -0.5px; width: calc(100% + 1px); top: -4px;" class="TableCellTopBorderHandle BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; bottom: 0px; overflow: visible; width: 7px; top: -0.5px; z-index: 3; background-repeat: repeat; height: calc(100% + 1px); cursor: url("https://res-1-sdf.cdn.office.net:443/officeonline/we/s/hD55E5E9C2AD2E4F5_resources/1033/ColResize.cur"), pointer; right: -4px;" class="TableColumnResizeHandle BCX8 SCXW253660117"></div><div style="margin: -3px 0px 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; height: 7px; z-index: 3; background-repeat: repeat; background-color: transparent; left: -0.5px; width: calc(100% + 1px); cursor: url("https://res-1-sdf.cdn.office.net:443/officeonline/we/s/h1E5273DBAA04AEF6_resources/1033/RowResize.cur"), pointer; bottom: -4px;" class="TableInsertRowGapBlank BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px 6px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible;" class="TableCellContent BCX8 SCXW253660117"><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; cursor: text; clear: both; position: relative; direction: ltr;" class="OutlineElement Ltr BCX8 SCXW253660117"><p style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;" class="Paragraph SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(255, 255, 255); font-size: 14pt; line-height: 24.4125px; font-family: Aptos_MSFontService, Aptos_MSFontService_EmbeddedFont, Aptos_MSFontService_MSFontService, sans-serif; font-weight: bold;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8">Announcement </span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 14pt; line-height: 24.4125px; font-family: Aptos_MSFontService, Aptos_MSFontService_EmbeddedFont, Aptos_MSFontService_MSFontService, sans-serif; color: rgb(255, 255, 255);" data-ccp-props="{"201341983":0,"335559740":279}" class="EOP SCXW253660117 BCX8"> </span></p></div></div></td></tr><tr style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; height: 20px;" aria-rowindex="3" role="row" class="TableRow BCX8 SCXW253660117"><td style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; vertical-align: top; overflow: visible; position: relative; background-color: transparent; width: 624px; border: 1px solid rgb(0, 0, 0);" colspan="2" data-celllook="4369" role="rowheader" class="TableCellBorderSelection FirstCol LastCol LastRow BCX8 SCXW253660117"><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; display: flex; overflow: visible; width: 15px; height: 10px; cursor: pointer; z-index: 5; background-color: transparent; bottom: -5.5px; left: -15.5px;" class="TableHoverRowHandle BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; width: 7px; z-index: 1; background-repeat: repeat; cursor: pointer; height: calc(100% + 1px); left: -4px; top: -0.5px;" class="TableCellLeftBorderHandle BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; height: 7px; z-index: 1; background-repeat: repeat; background-color: transparent; cursor: pointer; left: -0.5px; width: calc(100% + 1px); top: -4px;" class="TableCellTopBorderHandle BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; bottom: 0px; overflow: visible; width: 7px; top: -0.5px; z-index: 3; background-repeat: repeat; height: calc(100% + 1px); cursor: url("https://res-1-sdf.cdn.office.net:443/officeonline/we/s/hD55E5E9C2AD2E4F5_resources/1033/ColResize.cur"), pointer; right: -4px;" class="TableColumnResizeHandle BCX8 SCXW253660117"></div><div style="margin: -3px 0px 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; position: absolute; overflow: visible; height: 7px; z-index: 3; background-repeat: repeat; background-color: transparent; left: -0.5px; width: calc(100% + 1px); cursor: url("https://res-1-sdf.cdn.office.net:443/officeonline/we/s/h1E5273DBAA04AEF6_resources/1033/RowResize.cur"), pointer; bottom: -4px;" class="TableInsertRowGapBlank BCX8 SCXW253660117"></div><div style="margin: 0px; padding: 0px 6px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible;" class="TableCellContent BCX8 SCXW253660117"><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; cursor: text; clear: both; position: relative; direction: ltr;" class="OutlineElement Ltr BCX8 SCXW253660117"><p style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;" class="Paragraph SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(0, 0, 0); font-size: 12pt; line-height: 23.7333px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8">Hello </span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 12pt; line-height: 23.7333px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(0, 0, 0);" data-ccp-props="{"201341983":1,"335559740":356}" class="EOP SCXW253660117 BCX8"> </span></p></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; cursor: text; clear: both; position: relative; direction: ltr;" class="OutlineElement Ltr BCX8 SCXW253660117"><p style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;" class="Paragraph SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(0, 0, 0); font-size: 12pt; line-height: 23.7333px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8"></span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 12pt; line-height: 23.7333px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(0, 0, 0);" data-ccp-props="{"201341983":1,"335559740":356}" class="EOP SCXW253660117 BCX8"> </span></p></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; cursor: text; clear: both; position: relative; direction: ltr;" class="OutlineElement Ltr BCX8 SCXW253660117"><p style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;" class="Paragraph SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(0, 0, 0); font-size: 12pt; line-height: 23.7333px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8">[Brief description of change]</span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 12pt; line-height: 23.7333px; font-family: WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(0, 0, 0);" class="LineBreakBlob BlobObject DragDrop SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; white-space: pre !important;" class="SCXW253660117 BCX8"> </span><br style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; white-space: pre !important;" class="SCXW253660117 BCX8"></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(0, 0, 0); font-size: 12pt; line-height: 23.7333px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif;" class="TextRun EmptyTextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 12pt; line-height: 23.7333px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(0, 0, 0);" data-ccp-props="{"201341983":1,"335559740":356}" class="EOP SCXW253660117 BCX8"> </span></p></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; cursor: text; clear: both; position: relative; direction: ltr;" class="OutlineElement Ltr BCX8 SCXW253660117"><p style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;" class="Paragraph SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(0, 0, 0); font-size: 12pt; line-height: 23.7333px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8">[What changed and how it </span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8">benefits</span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8"> </span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; background-repeat: repeat-x; background-position: left bottom; background-image: var(--urlSpellingErrorV2); border-bottom: 1px solid transparent;" class="NormalTextRun SpellingErrorV2Themed SCXW253660117 BCX8">devs</span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8">]</span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 12pt; line-height: 23.7333px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(0, 0, 0);" data-ccp-props="{"201341983":1,"335559740":356}" class="EOP SCXW253660117 BCX8"> </span></p></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; cursor: text; clear: both; position: relative; direction: ltr;" class="OutlineElement Ltr BCX8 SCXW253660117"><p style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;" class="Paragraph SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(0, 0, 0); font-size: 12pt; line-height: 21px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8"></span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 12pt; line-height: 21px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(0, 0, 0);" data-ccp-props="{"201341983":1,"335559740":291}" class="EOP SCXW253660117 BCX8"> </span></p></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; cursor: text; clear: both; position: relative; direction: ltr;" class="OutlineElement Ltr BCX8 SCXW253660117"><p style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;" class="Paragraph SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(0, 0, 0); font-size: 12pt; line-height: 21px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8">[Any action needed by devs]</span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 12pt; line-height: 21px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(0, 0, 0);" data-ccp-props="{"201341983":1,"335559740":291}" class="EOP SCXW253660117 BCX8"> </span></p></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; cursor: text; clear: both; position: relative; direction: ltr;" class="OutlineElement Ltr BCX8 SCXW253660117"><p style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;" class="Paragraph SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(0, 0, 0); font-size: 12pt; line-height: 21px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8"></span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 12pt; line-height: 21px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(0, 0, 0);" data-ccp-props="{"201341983":1,"335559740":291}" class="EOP SCXW253660117 BCX8"> </span></p></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; cursor: text; clear: both; position: relative; direction: ltr;" class="OutlineElement Ltr BCX8 SCXW253660117"><p style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;" class="Paragraph SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(0, 0, 0); font-size: 12pt; line-height: 21px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8">[Link to Documentation ]</span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 12pt; line-height: 21px; font-family: WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(0, 0, 0);" class="LineBreakBlob BlobObject DragDrop SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; white-space: pre !important;" class="SCXW253660117 BCX8"> </span><br style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; white-space: pre !important;" class="SCXW253660117 BCX8"></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(0, 0, 0); font-size: 12pt; line-height: 21px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8"> </span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 12pt; line-height: 21px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(0, 0, 0);" data-ccp-props="{"201341983":1,"335559740":291}" class="EOP SCXW253660117 BCX8"> </span></p></div><div style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow: visible; cursor: text; clear: both; position: relative; direction: ltr;" class="OutlineElement Ltr BCX8 SCXW253660117"><p style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;" class="Paragraph SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(0, 0, 0); font-size: 12pt; line-height: 23.7333px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif;" class="TextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent;" class="NormalTextRun SCXW253660117 BCX8">[What comes next if something comes next]</span></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 12pt; line-height: 23.7333px; font-family: WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(0, 0, 0);" class="LineBreakBlob BlobObject DragDrop SCXW253660117 BCX8"><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; white-space: pre !important;" class="SCXW253660117 BCX8"> </span><br style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; white-space: pre !important;" class="SCXW253660117 BCX8"></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-variant-ligatures: none !important; color: rgb(0, 0, 0); font-size: 12pt; line-height: 23.7333px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif;" class="TextRun EmptyTextRun SCXW253660117 BCX8" lang="EN-US" data-contrast="none"></span><span style="margin: 0px; padding: 0px; user-select: text; -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; font-size: 12pt; line-height: 23.7333px; font-family: "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif; color: rgb(0, 0, 0);" data-ccp-props="{"201341983":1,"335559740":356}" class="EOP SCXW253660117 BCX8"> </span></p></div></div></td></tr></tbody></table></div></div>', undefined, @@ -4597,9 +4584,9 @@ describe('wordOnlineHandler', () => { }); itChromeOnly('Test with multiple list items', () => { - runTest( + const [,] = runTest( '<div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle1 SCXW143175918 BCX8" role="list" start="1" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: decimal; overflow: visible;"><li data-leveltext="%1." data-font="" data-listid="3" data-list-defn-props="{"335552541":0,"335559683":0,"335559684":-1,"335559685":720,"335559991":360,"469769242":[65533,0,46],"469777803":"left","469777804":"%1.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="1" data-aria-level="1" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 24px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="1372804505" paraeid="{eda76604-e671-4d57-b201-b51196189a19}{123}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":720,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":360}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle1 SCXW143175918 BCX8" role="list" start="2" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: decimal; overflow: visible;"><li data-leveltext="%1." data-font="" data-listid="3" data-list-defn-props="{"335552541":0,"335559683":0,"335559684":-1,"335559685":720,"335559991":360,"469769242":[65533,0,46],"469777803":"left","469777804":"%1.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="2" data-aria-level="1" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 24px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="2030008708" paraeid="{6992e937-522a-4d72-bd0e-df82a2072fe7}{172}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle2 SCXW143175918 BCX8" role="list" start="1" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: lower-alpha; overflow: visible;"><li data-leveltext="%2." data-font="" data-listid="3" data-list-defn-props="{"335552541":0,"335559683":1,"335559684":-1,"335559685":1440,"335559991":360,"469769242":[65533,4,46],"469777803":"left","469777804":"%2.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="1" data-aria-level="2" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 72px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="1014254816" paraeid="{759c6a1b-b3fc-4831-bc8d-1354c2c5db98}{21}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":1440,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":360}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle2 SCXW143175918 BCX8" role="list" start="2" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: lower-alpha; overflow: visible;"><li data-leveltext="%2." data-font="" data-listid="3" data-list-defn-props="{"335552541":0,"335559683":1,"335559684":-1,"335559685":1440,"335559991":360,"469769242":[65533,4,46],"469777803":"left","469777804":"%2.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="2" data-aria-level="2" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 72px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="1091958214" paraeid="{759c6a1b-b3fc-4831-bc8d-1354c2c5db98}{120}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":1440,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":360}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle3 SCXW143175918 BCX8" role="list" start="1" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: lower-roman; overflow: visible;"><li data-leveltext="%3." data-font="" data-listid="3" data-list-defn-props="{"335552541":0,"335559683":2,"335559684":-1,"335559685":2160,"335559991":180,"469769242":[65533,2,46],"469777803":"right","469777804":"%3.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="1" data-aria-level="3" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 132px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="839056829" paraeid="{759c6a1b-b3fc-4831-bc8d-1354c2c5db98}{215}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":2160,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":180}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle3 SCXW143175918 BCX8" role="list" start="2" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: lower-roman; overflow: visible;"><li data-leveltext="%3." data-font="" data-listid="3" data-list-defn-props="{"335552541":0,"335559683":2,"335559684":-1,"335559685":2160,"335559991":180,"469769242":[65533,2,46],"469777803":"right","469777804":"%3.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="2" data-aria-level="3" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 132px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="1810158270" paraeid="{f908626c-78ed-46b1-8200-5622a1ffe344}{44}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":2160,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":180}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle2 SCXW143175918 BCX8" role="list" start="3" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: lower-alpha; overflow: visible;"><li data-leveltext="%2." data-font="" data-listid="3" data-list-defn-props="{"335552541":0,"335559683":1,"335559684":-1,"335559685":1440,"335559991":360,"469769242":[65533,4,46],"469777803":"left","469777804":"%2.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="3" data-aria-level="2" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 72px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="139753695" paraeid="{f908626c-78ed-46b1-8200-5622a1ffe344}{124}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":1440,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":360}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle2 SCXW143175918 BCX8" role="list" start="4" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: lower-alpha; overflow: visible;"><li data-leveltext="%2." data-font="" data-listid="3" data-list-defn-props="{"335552541":0,"335559683":1,"335559684":-1,"335559685":1440,"335559991":360,"469769242":[65533,4,46],"469777803":"left","469777804":"%2.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="4" data-aria-level="2" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 72px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="407695755" paraeid="{f908626c-78ed-46b1-8200-5622a1ffe344}{209}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":1440,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":360}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle1 SCXW143175918 BCX8" role="list" start="3" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: decimal; overflow: visible;"><li data-leveltext="%1." data-font="" data-listid="3" data-list-defn-props="{"335552541":0,"335559683":0,"335559684":-1,"335559685":720,"335559991":360,"469769242":[65533,0,46],"469777803":"left","469777804":"%1.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="3" data-aria-level="1" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 24px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="620995694" paraeid="{9878f376-5df8-4e62-ba39-9c6a1817f7b5}{34}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":720,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":360}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle1 SCXW143175918 BCX8" role="list" start="4" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: decimal; overflow: visible;"><li data-leveltext="%1." data-font="" data-listid="3" data-list-defn-props="{"335552541":0,"335559683":0,"335559684":-1,"335559685":720,"335559991":360,"469769242":[65533,0,46],"469777803":"left","469777804":"%1.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="4" data-aria-level="1" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 24px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="406328277" paraeid="{9878f376-5df8-4e62-ba39-9c6a1817f7b5}{119}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":720,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":360}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="502077388" paraeid="{2147c5de-cb36-425c-ad05-d9081387dfe2}{97}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 10.6667px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;"></span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></div><div class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="338729307" paraeid="{6992e937-522a-4d72-bd0e-df82a2072fe7}{209}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 10.6667px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;"></span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></div><div class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="1060692795" paraeid="{9878f376-5df8-4e62-ba39-9c6a1817f7b5}{212}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 10.6667px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":0,"335559737":0,"335559738":0,"335559739":160,"335559740":279}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></div><div class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="1868174151" paraeid="{5ff1bd63-a438-4abf-b8f6-ee8fc30ba819}{1}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 10.6667px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;"></span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle1 SCXW143175918 BCX8" role="list" start="1" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: decimal; overflow: visible;"><li data-leveltext="%1." data-font="" data-listid="4" data-list-defn-props="{"335552541":0,"335559683":0,"335559684":-1,"335559685":720,"335559991":360,"469769242":[65533,0,46],"469777803":"left","469777804":"%1.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="1" data-aria-level="1" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 24px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="963440506" paraeid="{b77ffaa8-6f5e-4079-83c2-373c935ff7d8}{1}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":720,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":360}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="955920880" paraeid="{b77ffaa8-6f5e-4079-83c2-373c935ff7d8}{129}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 10.6667px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":0,"335559737":0,"335559738":0,"335559739":160,"335559740":279}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle1 SCXW143175918 BCX8" role="list" start="2" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: decimal; overflow: visible;"><li data-leveltext="%1." data-font="" data-listid="4" data-list-defn-props="{"335552541":0,"335559683":0,"335559684":-1,"335559685":720,"335559991":360,"469769242":[65533,0,46],"469777803":"left","469777804":"%1.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="2" data-aria-level="1" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 24px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="1825308776" paraeid="{b77ffaa8-6f5e-4079-83c2-373c935ff7d8}{173}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":720,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":360}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="326656423" paraeid="{5ff1bd63-a438-4abf-b8f6-ee8fc30ba819}{107}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 10.6667px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;"></span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></div><div class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="99360513" paraeid="{6b98cfd7-eaec-4e75-8b54-ad1b76c09801}{11}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 10.6667px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":0,"335559737":0,"335559738":0,"335559739":160,"335559740":279}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></div><div class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="96460220" paraeid="{5ff1bd63-a438-4abf-b8f6-ee8fc30ba819}{157}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 10.6667px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;"></span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle1 SCXW143175918 BCX8" role="list" start="1" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: decimal; overflow: visible;"><li data-leveltext="%1." data-font="" data-listid="5" data-list-defn-props="{"335552541":0,"335559683":0,"335559684":-1,"335559685":720,"335559991":360,"469769242":[65533,0,46],"469777803":"left","469777804":"%1.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="1" data-aria-level="1" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 24px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="927907704" paraeid="{6b98cfd7-eaec-4e75-8b54-ad1b76c09801}{55}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":720,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":360}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle1 SCXW143175918 BCX8" role="list" start="2" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: decimal; overflow: visible;"><li data-leveltext="%1." data-font="" data-listid="5" data-list-defn-props="{"335552541":0,"335559683":0,"335559684":-1,"335559685":720,"335559991":360,"469769242":[65533,0,46],"469777803":"left","469777804":"%1.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="2" data-aria-level="1" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 24px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="1760568901" paraeid="{6b98cfd7-eaec-4e75-8b54-ad1b76c09801}{169}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":720,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":360}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="660136120" paraeid="{0bd5b816-7be0-4589-b756-3f1f2a595131}{30}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 10.6667px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;"></span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></div><div class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="934524283" paraeid="{33f05aa7-528d-4f60-a926-5a5ab49bb8f6}{7}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 10.6667px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":0,"335559737":0,"335559738":0,"335559739":160,"335559740":279}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle1 SCXW143175918 BCX8" role="list" start="1" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: decimal; overflow: visible;"><li data-leveltext="%1." data-font="" data-listid="6" data-list-defn-props="{"335552541":0,"335559683":0,"335559684":-1,"335559685":720,"335559991":360,"469769242":[65533,0,46],"469777803":"left","469777804":"%1.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="1" data-aria-level="1" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 24px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="1629030307" paraeid="{33f05aa7-528d-4f60-a926-5a5ab49bb8f6}{51}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":720,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":360}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div><div class="ListContainerWrapper SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; position: relative; color: rgb(0, 0, 0); font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><ol class="NumberListStyle1 SCXW143175918 BCX8" role="list" start="2" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; cursor: text; list-style-type: decimal; overflow: visible;"><li data-leveltext="%1." data-font="" data-listid="6" data-list-defn-props="{"335552541":0,"335559683":0,"335559684":-1,"335559685":720,"335559991":360,"469769242":[65533,0,46],"469777803":"left","469777804":"%1.","469777815":"hybridMultilevel"}" aria-setsize="-1" data-aria-posinset="2" data-aria-level="1" role="listitem" class="OutlineElement Ltr SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px 0px 0px 24px; padding: 0px; user-select: text; clear: both; cursor: text; overflow: visible; position: relative; direction: ltr; display: block; font-size: 12pt; font-family: Aptos, Aptos_MSFontService, sans-serif; vertical-align: baseline;"><p class="Paragraph SCXW143175918 BCX8" xml:lang="EN-US" lang="EN-US" paraid="1045937546" paraeid="{33f05aa7-528d-4f60-a926-5a5ab49bb8f6}{165}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; overflow-wrap: break-word; white-space: pre-wrap; font-weight: normal; font-style: normal; vertical-align: baseline; font-kerning: none; background-color: transparent; color: windowtext; text-align: left; text-indent: 0px;"><span data-contrast="auto" xml:lang="EN-US" lang="EN-US" class="TextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-variant-ligatures: none !important; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"><span class="NormalTextRun SCXW143175918 BCX8" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text;">_</span></span><span class="EOP SCXW143175918 BCX8" data-ccp-props="{"134233117":false,"134233118":false,"201341983":0,"335551550":1,"335551620":1,"335559685":720,"335559737":0,"335559738":0,"335559739":160,"335559740":279,"335559991":360}" style="-webkit-user-drag: none; -webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; user-select: text; font-size: 12pt; line-height: 22.0875px; font-family: Aptos, Aptos_MSFontService, sans-serif;"> </span></p></li></ol></div>', - '<ol start="1" style="direction: ltr; margin-top: 0px; margin-bottom: 0px;"><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li><ol start="1" style="direction: ltr; margin-top: 0px; margin-bottom: 0px;"><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li><ol start="1" style="direction: ltr; margin-top: 0px; margin-bottom: 0px;"><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li></ol><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li></ol><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li></ol><div style="background-color: rgb(255, 255, 255); margin: 0px;"><p style="direction: ltr; text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px 0px 10.6667px; color: rgb(0, 0, 0);"><span style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; line-height: 22.0875px;"> </span></p></div><div style="background-color: rgb(255, 255, 255); margin: 0px;"><p style="direction: ltr; text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px 0px 10.6667px; color: rgb(0, 0, 0);"><span style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; line-height: 22.0875px;"> </span></p></div><div style="background-color: rgb(255, 255, 255); margin: 0px;"><p style="direction: ltr; text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px 0px 10.6667px; color: rgb(0, 0, 0);"><span style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; line-height: 22.0875px;">_ </span></p></div><div style="background-color: rgb(255, 255, 255); margin: 0px;"><p style="direction: ltr; text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px 0px 10.6667px; color: rgb(0, 0, 0);"><span style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; line-height: 22.0875px;"> </span></p></div><ol start="1" style="direction: ltr; margin-top: 0px; margin-bottom: 0px;"><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li></ol><div style="background-color: rgb(255, 255, 255); margin: 0px;"><p style="direction: ltr; text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px 0px 10.6667px; color: rgb(0, 0, 0);"><span style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; line-height: 22.0875px;">_ </span></p></div><ol start="2" style="direction: ltr; margin-top: 0px; margin-bottom: 0px;"><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li></ol><div style="background-color: rgb(255, 255, 255); margin: 0px;"><p style="direction: ltr; text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px 0px 10.6667px; color: rgb(0, 0, 0);"><span style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; line-height: 22.0875px;"> </span></p></div><div style="background-color: rgb(255, 255, 255); margin: 0px;"><p style="direction: ltr; text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px 0px 10.6667px; color: rgb(0, 0, 0);"><span style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; line-height: 22.0875px;">_ </span></p></div><div style="background-color: rgb(255, 255, 255); margin: 0px;"><p style="direction: ltr; text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px 0px 10.6667px; color: rgb(0, 0, 0);"><span style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; line-height: 22.0875px;"> </span></p></div><ol start="1" style="direction: ltr; margin-top: 0px; margin-bottom: 0px;"><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li></ol><div style="background-color: rgb(255, 255, 255); margin: 0px;"><p style="direction: ltr; text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px 0px 10.6667px; color: rgb(0, 0, 0);"><span style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; line-height: 22.0875px;"> </span></p></div><div style="background-color: rgb(255, 255, 255); margin: 0px;"><p style="direction: ltr; text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px 0px 10.6667px; color: rgb(0, 0, 0);"><span style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; line-height: 22.0875px;">_ </span></p></div><ol start="1" style="direction: ltr; margin-top: 0px; margin-bottom: 0px;"><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li><li style="font-family: Aptos, Aptos_MSFontService, sans-serif; font-size: 12pt; direction: ltr; margin-top: 0px; margin-bottom: 0px;"><p role="presentation" style="text-align: left; text-indent: 0px; white-space: pre-wrap; margin: 0px; color: rgb(0, 0, 0);"><span style="line-height: 22.0875px;">_ </span></p></li></ol>', + undefined, { blockGroupType: 'Document', blocks: [ @@ -4655,7 +4642,13 @@ describe('wordOnlineHandler', () => { levels: [ { listType: 'OL', - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + paddingLeft: '1em', + }, dataset: {}, }, ], @@ -4667,7 +4660,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '24px', + }, }, { blockType: 'BlockGroup', @@ -4721,7 +4720,13 @@ describe('wordOnlineHandler', () => { levels: [ { listType: 'OL', - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + paddingLeft: '1em', + }, dataset: {}, }, ], @@ -4733,7 +4738,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '24px', + }, }, { blockType: 'BlockGroup', @@ -4792,12 +4803,19 @@ describe('wordOnlineHandler', () => { marginTop: '0px', marginRight: '0px', marginBottom: '0px', + paddingLeft: '1em', }, dataset: {}, }, { listType: 'OL', - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + paddingLeft: '1em', + }, dataset: {}, }, ], @@ -4809,7 +4827,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '72px', + }, }, { blockType: 'BlockGroup', @@ -4868,12 +4892,19 @@ describe('wordOnlineHandler', () => { marginTop: '0px', marginRight: '0px', marginBottom: '0px', + paddingLeft: '1em', }, dataset: {}, }, { listType: 'OL', - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + paddingLeft: '1em', + }, dataset: {}, }, ], @@ -4885,7 +4916,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '72px', + }, }, { blockType: 'BlockGroup', @@ -4944,6 +4981,7 @@ describe('wordOnlineHandler', () => { marginTop: '0px', marginRight: '0px', marginBottom: '0px', + paddingLeft: '1em', }, dataset: {}, }, @@ -4954,12 +4992,19 @@ describe('wordOnlineHandler', () => { marginTop: '0px', marginRight: '0px', marginBottom: '0px', + paddingLeft: '1em', }, dataset: {}, }, { listType: 'OL', - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + paddingLeft: '1em', + }, dataset: {}, }, ], @@ -4971,7 +5016,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '132px', + }, }, { blockType: 'BlockGroup', @@ -5030,6 +5081,7 @@ describe('wordOnlineHandler', () => { marginTop: '0px', marginRight: '0px', marginBottom: '0px', + paddingLeft: '1em', }, dataset: {}, }, @@ -5040,12 +5092,19 @@ describe('wordOnlineHandler', () => { marginTop: '0px', marginRight: '0px', marginBottom: '0px', + paddingLeft: '1em', }, dataset: {}, }, { listType: 'OL', - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + paddingLeft: '1em', + }, dataset: {}, }, ], @@ -5057,7 +5116,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '132px', + }, }, { blockType: 'BlockGroup', @@ -5116,12 +5181,19 @@ describe('wordOnlineHandler', () => { marginTop: '0px', marginRight: '0px', marginBottom: '0px', + paddingLeft: '1em', }, dataset: {}, }, { listType: 'OL', - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + paddingLeft: '1em', + }, dataset: {}, }, ], @@ -5133,7 +5205,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '72px', + }, }, { blockType: 'BlockGroup', @@ -5192,12 +5270,19 @@ describe('wordOnlineHandler', () => { marginTop: '0px', marginRight: '0px', marginBottom: '0px', + paddingLeft: '1em', }, dataset: {}, }, { listType: 'OL', - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + paddingLeft: '1em', + }, dataset: {}, }, ], @@ -5209,7 +5294,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '72px', + }, }, { blockType: 'BlockGroup', @@ -5263,7 +5354,13 @@ describe('wordOnlineHandler', () => { levels: [ { listType: 'OL', - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + paddingLeft: '1em', + }, dataset: {}, }, ], @@ -5275,7 +5372,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '24px', + }, }, { blockType: 'BlockGroup', @@ -5329,7 +5432,13 @@ describe('wordOnlineHandler', () => { levels: [ { listType: 'OL', - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + paddingLeft: '1em', + }, dataset: {}, }, ], @@ -5341,7 +5450,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '24px', + }, }, { blockType: 'BlockGroup', @@ -5610,7 +5725,9 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', marginTop: '0px', + marginRight: '0px', marginBottom: '0px', + paddingLeft: '1em', startNumberOverride: 1, }, dataset: {}, @@ -5624,7 +5741,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '24px', + }, }, { blockType: 'BlockGroup', @@ -5740,7 +5863,13 @@ describe('wordOnlineHandler', () => { levels: [ { listType: 'OL', - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + paddingLeft: '1em', + }, dataset: {}, }, ], @@ -5752,7 +5881,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '24px', + }, }, { blockType: 'BlockGroup', @@ -5971,7 +6106,9 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', marginTop: '0px', + marginRight: '0px', marginBottom: '0px', + paddingLeft: '1em', startNumberOverride: 1, }, dataset: {}, @@ -5985,7 +6122,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '24px', + }, }, { blockType: 'BlockGroup', @@ -6039,7 +6182,13 @@ describe('wordOnlineHandler', () => { levels: [ { listType: 'OL', - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + paddingLeft: '1em', + }, dataset: {}, }, ], @@ -6051,7 +6200,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '24px', + }, }, { blockType: 'BlockGroup', @@ -6220,7 +6375,9 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', marginTop: '0px', + marginRight: '0px', marginBottom: '0px', + paddingLeft: '1em', startNumberOverride: 1, }, dataset: {}, @@ -6234,7 +6391,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '24px', + }, }, { blockType: 'BlockGroup', @@ -6288,7 +6451,13 @@ describe('wordOnlineHandler', () => { levels: [ { listType: 'OL', - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + paddingLeft: '1em', + }, dataset: {}, }, ], @@ -6300,7 +6469,13 @@ describe('wordOnlineHandler', () => { fontSize: '12pt', }, }, - format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + format: { + direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '24px', + }, }, ], }, diff --git a/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts b/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts index 4b4e7f3be7a2..b3d2dbd2a677 100644 --- a/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts +++ b/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts @@ -64,4 +64,18 @@ export type ExperimentalFeature = /** * Transform the table border colors when switching from light to dark mode */ - | 'TransformTableBorderColors'; + | 'TransformTableBorderColors' + + /** + * When the editor content div is inside a Shadow DOM, enable shadow root detection + * in DOMHelper so that selection, focus, and element appending work correctly within + * the shadow boundary. + */ + | 'ShadowDom' + + /** + * Strip invisible unicode characters (U+E0000 to U+EFFFF) from text segments during DOM to Model conversion. + * These characters can be used to hide text in HTML and may cause unexpected behavior. + * @see https://embracethered.com/blog/posts/2024/hiding-and-finding-text-with-unicode-tags/ + */ + | 'FilterInvisibleUnicode'; diff --git a/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts b/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts index 1fd76ba4f0c7..d5af2302f27f 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts @@ -127,4 +127,23 @@ export interface DOMHelper { * @returns An array of Ranges that match the search criteria */ getRangesByText(text: string, matchCase: boolean, wholeWord: boolean): Range[]; + + /** + * Get the current selection range, handling shadow DOM StaticRange conversion. + * Returns a live Range in all browsers. + */ + getSelectionRange(): Range | null; + + /** + * Set the selection to the given range, handling browser differences for shadow DOM. + * @param range The range to set + * @param isReverted Whether the selection is reverted (focus before anchor) + */ + setSelectionRange(range: Range, isReverted?: boolean): void; + + /** + * Append an element to the correct root container (shadow root or document.body) + * @param element The element to append + */ + appendToRoot(element: HTMLElement): void; } diff --git a/versions.json b/versions.json index 9e36e25278bb..5224e17d1f84 100644 --- a/versions.json +++ b/versions.json @@ -1,6 +1,6 @@ { "react": "9.0.4", - "main": "9.52.0", + "main": "9.53.0", "legacyAdapter": "8.66.0", "overrides": {} } diff --git a/yarn.lock b/yarn.lock index 652d7587dfbf..84e7d7242f15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2935,9 +2935,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-uri@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" - integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== + version "3.1.2" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.2.tgz#8af3d4fc9d3e71b11572cc2673b514a7d1a8c8ec" + integrity sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ== fastq@^1.6.0: version "1.15.0" @@ -6651,9 +6651,9 @@ thunky@^1.0.2: integrity sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow== tmp@^0.2.1: - version "0.2.4" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.4.tgz#c6db987a2ccc97f812f17137b36af2b6521b0d13" - integrity sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ== + version "0.2.7" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.7.tgz#26f4db11d1601ce8012dcb8a798ece1c06a99059" + integrity sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw== to-fast-properties@^2.0.0: version "2.0.0"