@atlaskit/editor-plugin-emoji
Version:
Emoji plugin for @atlaskit/editor-core
173 lines (172 loc) • 6.94 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { createRule } from '@atlaskit/editor-common/utils';
import { createPlugin, leafNodeReplacementCharacter } from '@atlaskit/prosemirror-input-rules';
let matcher;
export function inputRulePlugin(schema, editorAnalyticsAPI, pluginInjectionApi, disableAutoformat) {
if (disableAutoformat) {
return;
}
if (schema.nodes.emoji) {
initMatcher(pluginInjectionApi);
const asciiEmojiRule = createRule(AsciiEmojiMatcher.REGEX, inputRuleHandler(editorAnalyticsAPI));
return new SafePlugin(createPlugin('emoji', [asciiEmojiRule]));
}
return;
}
function initMatcher(pluginInjectionApi) {
pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : pluginInjectionApi.emoji.sharedState.onChange(({
nextSharedState
}) => {
const emojiProvider = nextSharedState === null || nextSharedState === void 0 ? void 0 : nextSharedState.emojiProvider;
if (emojiProvider) {
emojiProvider.getAsciiMap().then(map => {
matcher = new RecordingAsciiEmojiMatcher(emojiProvider, map);
});
}
});
}
const inputRuleHandler = editorAnalyticsAPI => (state, matchParts, start, end) => {
if (!matcher) {
return null;
}
const match = matcher.match(matchParts);
if (match) {
const transactionCreator = new AsciiEmojiTransactionCreator(state, match, start, end, editorAnalyticsAPI);
return transactionCreator.create();
}
return null;
};
const REGEX_LEADING_CAPTURE_INDEX = 1;
const REGEX_EMOJI_LEADING_PARENTHESES = 2;
const REGEX_EMOJI_ASCII_CAPTURE_INDEX = 3;
const REGEX_TRAILING_CAPTURE_INDEX = 4;
const getLeadingString = (match, withParenthesis = true) => match[REGEX_LEADING_CAPTURE_INDEX] + (withParenthesis ? match[REGEX_EMOJI_LEADING_PARENTHESES] : '');
const getLeadingStringWithoutParentheses = match => getLeadingString(match, false);
const getAscii = (match, withParentheses = false) => (withParentheses ? match[REGEX_EMOJI_LEADING_PARENTHESES] : '') + match[REGEX_EMOJI_ASCII_CAPTURE_INDEX].trim();
const getAsciiWithParentheses = matchParts => getAscii(matchParts, true);
const getTrailingString = match => match[REGEX_TRAILING_CAPTURE_INDEX] || '';
class AsciiEmojiMatcher {
constructor(asciiToEmojiMap) {
this.asciiToEmojiMap = asciiToEmojiMap;
}
match(matchParts) {
return this.getAsciiEmojiMatch(getLeadingStringWithoutParentheses(matchParts), getAsciiWithParentheses(matchParts), getTrailingString(matchParts)) || this.getAsciiEmojiMatch(getLeadingString(matchParts), getAscii(matchParts), getTrailingString(matchParts));
}
getAsciiEmojiMatch(leading, ascii, trailing) {
const emoji = this.asciiToEmojiMap.get(ascii);
return emoji ? {
emoji,
leadingString: leading,
trailingString: trailing
} : undefined;
}
}
/**
* A matcher that will record ascii matches as usages of the matched emoji.
*/
/**
* This regex matches 2 scenarios:
* 1. an emoticon starting with a colon character (e.g. :D => 😃)
* 2. an emoticon not starting with a colon character (e.g. 8-D => 😎)
*
* Explanation (${leafNodeReplacementCharacter} is replaced with character \ufffc)
*
* 1st Capturing Group ((?:^|[\s\ufffc])(?:\(*?))
* Non-capturing group (?:^|[\s\ufffc])
* 1st Alternative ^
* ^ asserts position at start of the string
* 2nd Alternative [\s\ufffc]
* matches a single character present in [\s\ufffc]
* Non-capturing group (?:\(*?)
* matches the character ( literally between zero and unlimited times, as few times as possible, expanding as needed (lazy)
* 2nd Capturing Group (\(?)
* matches a single ( if present
* 3rd Capturing Group ([^:\s\ufffc\(]\S{1,3}|:\S{1,3}( ))
* 1st Alternative [^:\s\ufffc\(]\S{1,3}
* matches a single character not present in [^:\s\ufffc\(] between 1 and 3 times, as many times as possible, giving back as needed (greedy)
* 2nd Alternative :\S{1,3}( )
* : matches the character : literally
* \S{1,3} matches any non-whitespace character between 1 and 3 times, as many times as possible, giving back as needed (greedy)
* 4th Capturing Group ( )
*
* See https://regex101.com/r/HRS9O2/4
*/
// New behavior: All emoticons require whitespace after them
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
_defineProperty(AsciiEmojiMatcher, "REGEX", new RegExp(`((?:^|[\\s${leafNodeReplacementCharacter}])(?:\\(*?))(\\(?)([^:\\s${leafNodeReplacementCharacter}\\(]\\S{1,3}|:\\S{1,3})([\\s\\t\\n])$`));
class RecordingAsciiEmojiMatcher extends AsciiEmojiMatcher {
constructor(emojiProvider, asciiToEmojiMap) {
super(asciiToEmojiMap);
this.emojiProvider = emojiProvider;
}
match(matchParts) {
const match = super.match(matchParts);
if (match && this.emojiProvider.recordSelection) {
this.emojiProvider.recordSelection(match.emoji);
}
return match;
}
}
class AsciiEmojiTransactionCreator {
constructor(state, match, start, end, editorAnalyticsAPI) {
this.state = state;
this.match = match;
this.start = start;
this.end = end;
this.editorAnalyticsAPI = editorAnalyticsAPI;
}
create() {
var _this$editorAnalytics;
const tr = this.state.tr.replaceWith(this.from, this.to, this.createNodes());
(_this$editorAnalytics = this.editorAnalyticsAPI) === null || _this$editorAnalytics === void 0 ? void 0 : _this$editorAnalytics.attachAnalyticsEvent({
action: ACTION.INSERTED,
actionSubject: ACTION_SUBJECT.DOCUMENT,
actionSubjectId: ACTION_SUBJECT_ID.EMOJI,
attributes: {
inputMethod: INPUT_METHOD.ASCII
},
eventType: EVENT_TYPE.TRACK
})(tr);
return tr;
}
get from() {
return this.start + this.match.leadingString.length;
}
get to() {
return this.end;
}
createNodes() {
const nodes = [this.createEmojiNode()];
if (this.trailingTextNodeRequired()) {
nodes.push(this.createTrailingTextNode());
}
return nodes;
}
createEmojiNode() {
const {
emoji: emojiTypeNode
} = this.state.schema.nodes;
return emojiTypeNode.create(this.getEmojiNodeAttrs());
}
getEmojiNodeAttrs() {
const emoji = this.match.emoji;
return {
id: emoji.id,
shortName: emoji.shortName,
text: emoji.fallback || emoji.shortName
};
}
trailingTextNodeRequired() {
return this.match.trailingString.length > 0;
}
createTrailingTextNode() {
return this.state.schema.text(this.match.trailingString);
}
}
const plugins = (schema, providerFactory, featureFlags, editorAnalyticsAPI, pluginInjectionApi) => {
return [inputRulePlugin(schema, editorAnalyticsAPI, pluginInjectionApi)].filter(plugin => !!plugin);
};
export default plugins;