UNPKG

@atlaskit/editor-plugin-text-formatting

Version:

Text-formatting plugin for @atlaskit/editor-core

224 lines (214 loc) 8.55 kB
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))))); });