-
Notifications
You must be signed in to change notification settings - Fork 146
/
CustomReplace.ts
181 lines (161 loc) · 6.23 KB
/
CustomReplace.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import { PluginEventType, PositionType } from 'roosterjs-editor-types';
import type { CustomReplacement, EditorPlugin, IEditor, PluginEvent } from 'roosterjs-editor-types';
const makeReplacement = (
sourceString: string,
replacementHTML: string,
matchSourceCaseSensitive: boolean,
shouldReplace?: (
replacement: CustomReplacement,
content: string,
sourceEditor?: IEditor
) => boolean
): CustomReplacement => ({
sourceString,
replacementHTML,
matchSourceCaseSensitive,
shouldReplace,
});
const defaultReplacements: CustomReplacement[] = [
makeReplacement(':)', '🙂', true),
makeReplacement(';)', '😉', true),
makeReplacement(':O', '😲', true),
makeReplacement(':o', '😯', true),
makeReplacement('<3', '❤️', true),
];
/**
* Wrapper for CustomReplaceContentEditFeature that provides an API for updating the
* content edit feature
*/
export default class CustomReplacePlugin implements EditorPlugin {
private longestReplacementLength: number | null = null;
private editor: IEditor | null = null;
private replacements: CustomReplacement[] | null = null;
private replacementEndCharacters: Set<string> | null = null;
/**
* Create instance of CustomReplace plugin
* @param replacements Replacement rules. If not passed, a default replacement rule set will be applied
*/
constructor(replacements: CustomReplacement[] = defaultReplacements) {
this.updateReplacements(replacements);
}
/**
* Set the replacements that this plugin is looking for.
* @param newReplacements new set of replacements for this plugin
*/
updateReplacements(newReplacements: CustomReplacement[]) {
this.replacements = newReplacements;
this.longestReplacementLength = getLongestReplacementSourceLength(this.replacements);
this.replacementEndCharacters = getReplacementEndCharacters(this.replacements);
}
/**
* Get a friendly name of this plugin
*/
getName() {
return 'CustomReplace';
}
/**
* Initialize this plugin
* @param editor The editor instance
*/
public initialize(editor: IEditor): void {
this.editor = editor;
}
/**
* Dispose this plugin
*/
public dispose(): void {
this.editor = null;
}
/**
* Handle events triggered from editor
* @param event PluginEvent object
*/
public onPluginEvent(event: PluginEvent) {
if (event.eventType != PluginEventType.Input || !this.editor || this.editor.isInIME()) {
return;
}
// Exit early on input events that do not insert a replacement's final character.
if (!event.rawEvent.data || !this.replacementEndCharacters?.has(event.rawEvent.data)) {
return;
}
// Get the matching replacement
const searcher = this.editor.getContentSearcherOfCursor(event);
if (!searcher || this.longestReplacementLength == null) {
return;
}
const stringToSearch = searcher.getSubStringBefore(this.longestReplacementLength);
const replacement = this.getMatchingReplacement(stringToSearch);
if (
!replacement ||
(replacement.shouldReplace &&
searcher &&
!replacement.shouldReplace(replacement, searcher.getWordBefore(), this.editor))
) {
return;
}
// Reconstruct a selection of the text on the document that matches the
// replacement we selected.
const matchingText = searcher.getSubStringBefore(replacement.sourceString.length);
const matchingRange = searcher.getRangeFromText(matchingText, true /* exactMatch */);
// parse the html string off the dom and inline the resulting element.
const document = this.editor.getDocument();
const parsingSpan = document.createElement('span');
parsingSpan.innerHTML = this.editor.getTrustedHTMLHandler()(replacement.replacementHTML);
const nodeToInsert =
parsingSpan.childNodes.length == 1 ? parsingSpan.childNodes[0] : parsingSpan;
// Switch the node for the selection range
if (matchingRange) {
this.editor.addUndoSnapshot(
() => {
matchingRange.deleteContents();
matchingRange.insertNode(nodeToInsert);
this.editor?.select(nodeToInsert, PositionType.End);
},
undefined /*changeSource*/,
true /*canUndoByBackspace*/
);
}
}
private getMatchingReplacement(stringToSearch: string): CustomReplacement | null {
if (stringToSearch.length == 0 || !this.replacements) {
return null;
}
const originalStringToSearch = stringToSearch.replace(/\s/g, ' ');
const lowerCaseStringToSearch = originalStringToSearch.toLocaleLowerCase();
for (const replacement of this.replacements) {
const [sourceMatch, replacementMatch] = replacement.matchSourceCaseSensitive
? [originalStringToSearch, replacement.sourceString]
: [lowerCaseStringToSearch, replacement.sourceString.toLocaleLowerCase()];
if (
sourceMatch.substring(sourceMatch.length - replacementMatch.length) ==
replacementMatch
) {
return replacement;
}
}
return null;
}
}
function getLongestReplacementSourceLength(replacements: CustomReplacement[]): number {
return Math.max.apply(
null,
replacements.map(replacement => replacement.sourceString.length)
);
}
function getReplacementEndCharacters(replacements: CustomReplacement[]): Set<string> {
const endChars = new Set<string>();
for (const replacement of replacements) {
const sourceString = replacement.sourceString;
if (sourceString.length == 0) {
continue;
}
const lastChar = sourceString[sourceString.length - 1];
if (!replacement.matchSourceCaseSensitive) {
endChars.add(lastChar.toLocaleLowerCase());
endChars.add(lastChar.toLocaleUpperCase());
} else {
endChars.add(lastChar);
}
}
return endChars;
}