@atlaskit/editor-plugin-text-formatting
Version:
Text-formatting plugin for @atlaskit/editor-core
224 lines (214 loc) • 8.55 kB
JavaScript
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, PUNC, SYMBOL } from '@atlaskit/editor-common/analytics';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { createRule, inputRuleWithAnalytics } from '@atlaskit/editor-common/utils';
import { Selection } from '@atlaskit/editor-prosemirror/state';
import { createPlugin } from '@atlaskit/prosemirror-input-rules';
// We don't agressively upgrade single quotes to smart quotes because
// they may clash with an emoji. Only do that when we have a matching
// single quote, or a contraction.
// wrapped text
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var WRAPPED_SINGLE_QUOTE_REGEX = /(\s|^)'(\S+.*\S+)'$/;
// apostrophe
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var APOSTROPHE_REGEX = /(\w+)(')(\w+)$/;
// Product rules
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var ATLASSIAN_REGEX = /(\s+|^)(atlassian)(\s)$/;
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var JIRA_REGEX = /(\s+|^)(jira|JIRA)(\s)$/;
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var BITBUCKET_REGEX = /(\s+|^)(bitbucket|BitBucket)(\s)$/;
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var HIPCHAT_REGEX = /(\s+|^)(hipchat|HipChat)(\s)$/;
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var TRELLO_REGEX = /(\s+|^)(trello)(\s)$/;
// Symbol rules
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var ARROW_RIGHT_REGEX = /(\s+|^)(--?>)(\s)$/;
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var ARROW_LEFT_REGEX = /(\s+|^)(<--?)(\s)$/;
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var ARROW_DOUBLE_REGEX = /(\s+|^)(<->?)(\s)$/;
// Punctuation rules
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var DASH_REGEX = /(\s+|^)(--)(\s)$/;
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var ELLIPSIS_REGEX = /()(\.\.\.)$/;
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var OPEN_DOUBLE_QUOTE_REGEX = /((?:^|[\s\{\[\(\<'"\u2018\u201C]))(")$/;
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var CLOSE_DOUBLE_QUOTE_REGEX = /"$/;
/**
* Creates an InputRuleHandler that will match on a regular expression of the
* form `(prefix, content, suffix?)`, and replace it with some given text,
* maintaining prefix and suffix around the replacement.
*
* @param text text to replace with
*/
function replaceTextUsingCaptureGroup(text) {
return function (state, match, start, end) {
var _match = _slicedToArray(match, 4),
prefix = _match[1],
suffix = _match[3];
var replacement = text + (suffix || '');
var tr = state.tr,
$to = state.selection.$to;
var startPos = start + (prefix || '').length;
var safePos = Math.min(
// I know it is almost impossible to have a negative value at this point.
// But, this is JS #trustnoone
Math.max(startPos, 0), end);
tr.replaceWith(safePos, end, state.schema.text(replacement, $to.marks()));
tr.setSelection(Selection.near(tr.doc.resolve(tr.selection.to)));
return tr;
};
}
function createReplacementRule(to, from) {
return createRule(from, replaceTextUsingCaptureGroup(to));
}
/**
* Create replacement rules fiven a replacement map
* @param replMap - Replacement map
* @param trackingEventName - Analytics V2 tracking event name
* @param replacementRuleWithAnalytics - Analytics GAS V3 middleware for replacement and rules.
*/
function createReplacementRules(replMap, replacementRuleWithAnalytics) {
return Object.keys(replMap).map(function (replacement) {
var regex = replMap[replacement];
var rule = createReplacementRule(replacement, regex);
if (replacementRuleWithAnalytics) {
return replacementRuleWithAnalytics(replacement)(rule);
}
return rule;
});
}
// We don't agressively upgrade single quotes to smart quotes because
// they may clash with an emoji. Only do that when we have a matching
// single quote, or a contraction.
function createSingleQuotesRules() {
return [
// wrapped text
createRule(WRAPPED_SINGLE_QUOTE_REGEX, function (state, match, start, end) {
var OPEN_SMART_QUOTE_CHAR = '‘';
var CLOSED_SMART_QUOTE_CHAR = '’';
var _match2 = _slicedToArray(match, 3),
spacing = _match2[1],
innerContent = _match2[2];
// Edge case where match begins with some spacing. We need to add
// it back to the document
var openQuoteReplacement = spacing + OPEN_SMART_QUOTE_CHAR;
// End is not always where the closed quote is, edge case exists
// when there is spacing after the closed quote. We need to
// determine position of closed quote from the start position.
var positionOfClosedQuote = start + openQuoteReplacement.length + innerContent.length;
return state.tr.insertText(CLOSED_SMART_QUOTE_CHAR, positionOfClosedQuote, end).insertText(openQuoteReplacement, start, start + openQuoteReplacement.length);
}),
// apostrophe
createReplacementRule('’', APOSTROPHE_REGEX)];
}
/**
* Get replacement rules related to product
*/
function getProductRules(editorAnalyticsAPI) {
var productRuleWithAnalytics = function productRuleWithAnalytics(product) {
return inputRuleWithAnalytics(function (_, match) {
return {
action: ACTION.SUBSTITUTED,
actionSubject: ACTION_SUBJECT.TEXT,
actionSubjectId: ACTION_SUBJECT_ID.PRODUCT_NAME,
eventType: EVENT_TYPE.TRACK,
attributes: {
product: product,
originalSpelling: match[2]
}
};
}, editorAnalyticsAPI);
};
return createReplacementRules({
Atlassian: ATLASSIAN_REGEX,
Jira: JIRA_REGEX,
Bitbucket: BITBUCKET_REGEX,
Hipchat: HIPCHAT_REGEX,
Trello: TRELLO_REGEX
}, productRuleWithAnalytics);
}
/**
* Get replacement rules related to symbol
*/
function getSymbolRules(editorAnalyticsAPI) {
var symbolToString = {
'→': SYMBOL.ARROW_RIGHT,
'←': SYMBOL.ARROW_LEFT,
'↔︎': SYMBOL.ARROW_DOUBLE
};
var symbolRuleWithAnalytics = function symbolRuleWithAnalytics(symbol) {
return inputRuleWithAnalytics({
action: ACTION.SUBSTITUTED,
actionSubject: ACTION_SUBJECT.TEXT,
actionSubjectId: ACTION_SUBJECT_ID.SYMBOL,
eventType: EVENT_TYPE.TRACK,
attributes: {
symbol: symbolToString[symbol]
}
}, editorAnalyticsAPI);
};
return createReplacementRules({
'→': ARROW_RIGHT_REGEX,
'←': ARROW_LEFT_REGEX,
'↔︎': ARROW_DOUBLE_REGEX
}, symbolRuleWithAnalytics);
}
/**
* Get replacement rules related to punctuation
*/
function getPunctuationRules(editorAnalyticsAPI) {
var punctuationToString = _defineProperty({
'–': PUNC.DASH,
'…': PUNC.ELLIPSIS,
'”': PUNC.QUOTE_DOUBLE
}, PUNC.QUOTE_SINGLE, PUNC.QUOTE_SINGLE);
var punctuationRuleWithAnalytics = function punctuationRuleWithAnalytics(punctuation) {
return inputRuleWithAnalytics({
action: ACTION.SUBSTITUTED,
actionSubject: ACTION_SUBJECT.TEXT,
actionSubjectId: ACTION_SUBJECT_ID.PUNC,
eventType: EVENT_TYPE.TRACK,
attributes: {
punctuation: punctuationToString[punctuation]
}
}, editorAnalyticsAPI);
};
var dashEllipsisRules = createReplacementRules({
'–': DASH_REGEX,
'…': ELLIPSIS_REGEX
}, punctuationRuleWithAnalytics);
var doubleQuoteRules = createReplacementRules({
'“': OPEN_DOUBLE_QUOTE_REGEX,
'”': CLOSE_DOUBLE_QUOTE_REGEX
}, punctuationRuleWithAnalytics);
var singleQuoteRules = createSingleQuotesRules();
return [].concat(_toConsumableArray(dashEllipsisRules), _toConsumableArray(doubleQuoteRules), _toConsumableArray(singleQuoteRules.map(function (rule) {
return punctuationRuleWithAnalytics(PUNC.QUOTE_SINGLE)(rule);
})));
}
export default (function (editorAnalyticsAPI) {
return new SafePlugin(createPlugin('text-formatting:smart-input', [].concat(_toConsumableArray(getProductRules(editorAnalyticsAPI)), _toConsumableArray(getSymbolRules(editorAnalyticsAPI)), _toConsumableArray(getPunctuationRules(editorAnalyticsAPI)))));
});