Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Standalone Editor: Remove dependency to old code from api and plugins package #2349

Merged
merged 2 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions demo/scripts/controls/ContentModelEditorMainPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,14 @@ import SampleEntityPlugin from './sampleEntity/SampleEntityPlugin';
import SidePane from './sidePane/SidePane';
import TitleBar from './titleBar/TitleBar';
import { arrayPush } from 'roosterjs-editor-dom';
import { ContentModelEditPlugin, ContentModelPastePlugin } from 'roosterjs-content-model-plugins';
import { ContentModelRibbonPlugin } from './ribbonButtons/contentModel/ContentModelRibbonPlugin';
import { ContentModelSegmentFormat, Snapshots } from 'roosterjs-content-model-types';
import { createEmojiPlugin, createPasteOptionPlugin } from 'roosterjs-react';
import { EditorPlugin } from 'roosterjs-editor-types';
import { getDarkColor } from 'roosterjs-color-utils';
import { PartialTheme } from '@fluentui/react/lib/Theme';
import { trustedHTMLHandler } from '../utils/trustedHTMLHandler';
import {
ContentModelEditPlugin,
ContentModelPastePlugin,
EntityDelimiterPlugin,
} from 'roosterjs-content-model-plugins';
import {
ContentModelEditor,
ContentModelEditorOptions,
Expand Down Expand Up @@ -105,7 +101,6 @@ class ContentModelEditorMainPane extends MainPaneBase<ContentModelMainPaneState>
private pasteOptionPlugin: EditorPlugin;
private emojiPlugin: EditorPlugin;
private snapshotPlugin: ContentModelSnapshotPlugin;
private entityDelimiterPlugin: EntityDelimiterPlugin;
private toggleablePlugins: EditorPlugin[] | null = null;
private formatPainterPlugin: ContentModelFormatPainterPlugin;
private pastePlugin: ContentModelPastePlugin;
Expand Down Expand Up @@ -133,7 +128,6 @@ class ContentModelEditorMainPane extends MainPaneBase<ContentModelMainPaneState>
this.contentModelRibbonPlugin = new ContentModelRibbonPlugin();
this.pasteOptionPlugin = createPasteOptionPlugin();
this.emojiPlugin = createEmojiPlugin();
this.entityDelimiterPlugin = new EntityDelimiterPlugin();
this.formatPainterPlugin = new ContentModelFormatPainterPlugin();
this.pastePlugin = new ContentModelPastePlugin();
this.sampleEntityPlugin = new SampleEntityPlugin();
Expand Down Expand Up @@ -195,7 +189,6 @@ class ContentModelEditorMainPane extends MainPaneBase<ContentModelMainPaneState>
this.contentModelPanePlugin.getInnerRibbonPlugin(),
this.pasteOptionPlugin,
this.emojiPlugin,
this.entityDelimiterPlugin,
this.sampleEntityPlugin,
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { getObjectKeys } from 'roosterjs-content-model-dom';

/**
* @internal
*/
export interface LinkData {
/**
* Schema of a hyperlink
*/
scheme: string;

/**
* Original url of a hyperlink
*/
originalUrl: string;

/**
* Normalized url of a hyperlink
*/
normalizedUrl: string;
}

interface LinkMatchRule {
match: RegExp;
except?: RegExp;
normalizeUrl?: (url: string) => string;
}

// http exclude matching regex
// invalid URL example (in particular on IE and Edge):
// - http://www.bing.com%00, %00 before ? (question mark) is considered invalid. IE/Edge throws invalid argument exception
// - http://www.bing.com%1, %1 is invalid
// - http://www.bing.com%g, %g is invalid (IE and Edge expects a two hex value after a %)
// - http://www.bing.com%, % as ending is invalid (IE and Edge expects a two hex value after a %)
// All above % cases if they're after ? (question mark) is then considered valid again
// Similar for @, it needs to be after / (forward slash), or ? (question mark). Otherwise IE/Edge will throw security exception
// - http://www.bing.com@name, @name before ? (question mark) is considered invalid
// - http://www.bing.com/@name, is valid sine it is after / (forward slash)
// - http://www.bing.com?@name, is also valid since it is after ? (question mark)
// The regex below is essentially a break down of:
// ^[^?]+%[^0-9a-f]+ => to exclude URL like www.bing.com%%
// ^[^?]+%[0-9a-f][^0-9a-f]+ => to exclude URL like www.bing.com%1
// ^[^?]+%00 => to exclude URL like www.bing.com%00
// ^[^?]+%$ => to exclude URL like www.bing.com%
// ^https?:\/\/[^?\/]+@ => to exclude URL like http://www.bing.com@name
// ^www\.[^?\/]+@ => to exclude URL like www.bing.com@name
// , => to exclude url like www.bing,,com
const httpExcludeRegEx = /^[^?]+%[^0-9a-f]+|^[^?]+%[0-9a-f][^0-9a-f]+|^[^?]+%00|^[^?]+%$|^https?:\/\/[^?\/]+@|^www\.[^?\/]+@/i;

// via https://tools.ietf.org/html/rfc1035 Page 7
const labelRegEx = '[a-z0-9](?:[a-z0-9-]*[a-z0-9])?'; // We're using case insensitive regexps below so don't bother including A-Z
const domainNameRegEx = `(?:${labelRegEx}\\.)*${labelRegEx}`;
const domainPortRegEx = `${domainNameRegEx}(?:\\:[0-9]+)?`;
const domainPortWithUrlRegEx = `${domainPortRegEx}(?:[\\/\\?]\\S*)?`;

const linkMatchRules: Record<string, LinkMatchRule> = {
http: {
match: new RegExp(
`^(?:microsoft-edge:)?http:\\/\\/${domainPortWithUrlRegEx}|www\\.${domainPortWithUrlRegEx}`,
'i'
),
except: httpExcludeRegEx,
normalizeUrl: url =>
new RegExp('^(?:microsoft-edge:)?http:\\/\\/', 'i').test(url) ? url : 'http://' + url,
},
https: {
match: new RegExp(`^(?:microsoft-edge:)?https:\\/\\/${domainPortWithUrlRegEx}`, 'i'),
except: httpExcludeRegEx,
},
mailto: { match: new RegExp('^mailto:\\S+@\\S+\\.\\S+', 'i') },
notes: { match: new RegExp('^notes:\\/\\/\\S+', 'i') },
file: { match: new RegExp('^file:\\/\\/\\/?\\S+', 'i') },
unc: { match: new RegExp('^\\\\\\\\\\S+', 'i') },
ftp: {
match: new RegExp(
`^ftp:\\/\\/${domainPortWithUrlRegEx}|ftp\\.${domainPortWithUrlRegEx}`,
'i'
),
normalizeUrl: url => (new RegExp('^ftp:\\/\\/', 'i').test(url) ? url : 'ftp://' + url),
},
news: { match: new RegExp(`^news:(\\/\\/)?${domainPortWithUrlRegEx}`, 'i') },
telnet: { match: new RegExp(`^telnet:(\\/\\/)?${domainPortWithUrlRegEx}`, 'i') },
gopher: { match: new RegExp(`^gopher:\\/\\/${domainPortWithUrlRegEx}`, 'i') },
wais: { match: new RegExp(`^wais:(\\/\\/)?${domainPortWithUrlRegEx}`, 'i') },
};

/**
* @internal
* Try to match a given string with link match rules, return matched link
* @param url Input url to match
* @param option Link match option, exact or partial. If it is exact match, we need
* to check the length of matched link and url
* @param rules Optional link match rules, if not passed, only the default link match
* rules will be applied
* @returns The matched link data, or null if no match found.
* The link data includes an original url and a normalized url
*/
export function matchLink(url: string): LinkData | null {
if (url) {
for (const schema of getObjectKeys(linkMatchRules)) {
const rule = linkMatchRules[schema];
const matches = url.match(rule.match);
if (matches && matches[0] == url && (!rule.except || !rule.except.test(url))) {
return {
scheme: schema,
originalUrl: url,
normalizedUrl: rule.normalizeUrl ? rule.normalizeUrl(url) : url,
};
}
}
}

return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
InsertEntityOptions,
IStandaloneEditor,
} from 'roosterjs-content-model-types';
import type { Entity } from 'roosterjs-editor-types';

const BlockEntityTag = 'div';
const InlineEntityTag = 'span';
Expand Down Expand Up @@ -91,17 +90,12 @@ export default function insertEntity(
{
selectionOverride: typeof position === 'object' ? position : undefined,
changeSource: ChangeSource.InsertEntity,
getChangeData: () => {
// TODO: Remove this entity when we have standalone editor
const entity: Entity = {
wrapper,
type,
id: '',
isReadonly: true,
};

return entity;
},
getChangeData: () => ({
wrapper,
type,
id: '',
isReadonly: true,
}),
apiName: 'insertEntity',
}
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { adjustTrailingSpaceSelection } from '../../modelApi/selection/adjustTrailingSpaceSelection';
import { ChangeSource, getSelectedSegments, mergeModel } from 'roosterjs-content-model-core';
import { HtmlSanitizer, matchLink } from 'roosterjs-editor-dom';
import { matchLink } from '../../modelApi/link/matchLink';
import type { ContentModelLink, IStandaloneEditor } from 'roosterjs-content-model-types';
import {
addLink,
Expand Down Expand Up @@ -126,7 +126,6 @@ const createLink = (
};
};

// TODO: This is copied from original code. We may need to integrate this logic into matchLink() later.
function applyLinkPrefix(url: string): string {
if (!url) {
return url;
Expand All @@ -152,16 +151,6 @@ function applyLinkPrefix(url: string): string {
return prefix + url;
}

// TODO: This is copied from original code. However, ContentModel should be able to filter out malicious
// attributes later, so no need to use HtmlSanitizer here
function checkXss(link: string): string {
const sanitizer = new HtmlSanitizer();
const a = document.createElement('a');

a.href = link || '';

sanitizer.sanitize(a);
// We use getAttribute because some browsers will try to make the href property a valid link.
// This has unintended side effects when the link lacks a protocol.
return a.getAttribute('href') || '';
return link.match(/s\n*c\n*r\n*i\n*p\n*t\n*:/i) ? '' : link;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
"description": "Content Model for roosterjs (Under development)",
"dependencies": {
"tslib": "^2.3.1",
"roosterjs-editor-types": "",
"roosterjs-editor-dom": "",
"roosterjs-content-model-core": "",
"roosterjs-content-model-dom": "",
"roosterjs-content-model-types": ""
Expand Down
Loading
Loading