@atlaskit/editor-plugin-text-formatting
Version:
Text-formatting plugin for @atlaskit/editor-core
286 lines (277 loc) • 11.7 kB
JavaScript
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, inputRuleWithAnalytics } from '@atlaskit/editor-common/utils';
import { createPlugin, leafNodeReplacementCharacter } from '@atlaskit/prosemirror-input-rules';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
var ValidAutoformatChars = /*#__PURE__*/function (ValidAutoformatChars) {
ValidAutoformatChars["STRONG"] = "__";
ValidAutoformatChars["STRIKE"] = "~~";
ValidAutoformatChars["STRONG_MARKDOWN"] = "**";
ValidAutoformatChars["ITALIC_MARKDOWN"] = "*";
ValidAutoformatChars["ITALIC"] = "_";
ValidAutoformatChars["CODE"] = "`";
return ValidAutoformatChars;
}(ValidAutoformatChars || {});
export const ValidCombinations = {
[ValidAutoformatChars.STRIKE]: [
// e.g: _~~lol~~_
ValidAutoformatChars.ITALIC,
// e.g: __~~lol~~__
ValidAutoformatChars.STRONG,
// e.g: **~~lol~~**
ValidAutoformatChars.STRONG_MARKDOWN,
// e.g: *~~lol~~*
ValidAutoformatChars.ITALIC_MARKDOWN],
[ValidAutoformatChars.STRONG]: [
// e.g: ~~__lol__~~
ValidAutoformatChars.STRIKE,
// e.g: *__lol__*
ValidAutoformatChars.ITALIC_MARKDOWN],
[ValidAutoformatChars.STRONG_MARKDOWN]: [
// e.g: _**lol**_
ValidAutoformatChars.ITALIC,
// e.g: ~~**lol**~~
ValidAutoformatChars.STRIKE],
[ValidAutoformatChars.ITALIC_MARKDOWN]: [
// e.g: ~~*lol*~~
ValidAutoformatChars.STRIKE,
// e.g: __*lol*__
ValidAutoformatChars.STRONG],
[ValidAutoformatChars.ITALIC]: [
// e.g: ~~_lol_~~
ValidAutoformatChars.STRIKE,
// e.g: **_lol_**
ValidAutoformatChars.STRONG_MARKDOWN],
[ValidAutoformatChars.CODE]: [
// e.g: loko (`some code`
'( ']
};
function addMark(markType, _schema, char, api) {
return (state, _match, start, end) => {
var _schema$marks, _schema$marks$code;
const {
doc,
schema,
tr
} = state;
const textPrefix = state.doc.textBetween(start, start + char.length);
// fixes the following case: my `*name` is *
// expected result: should ignore special characters inside "code"
if (textPrefix !== char || schema !== null && schema !== void 0 && (_schema$marks = schema.marks) !== null && _schema$marks !== void 0 && (_schema$marks$code = _schema$marks.code) !== null && _schema$marks$code !== void 0 && _schema$marks$code.isInSet(doc.resolve(start + 1).marks())) {
if (!expValEquals('platform_editor_lovability_inline_code', 'isEnabled', true)) {
return null;
}
// if the prefix is not a character but the suffix is, continue
const suffix = state.doc.textBetween(end - char.length, end);
if (suffix !== char) {
return null;
}
}
// Prevent autoformatting across hardbreaks
let containsHardBreak;
doc.nodesBetween(start, end, node => {
if (node.type === schema.nodes.hardBreak) {
containsHardBreak = true;
return false;
}
return !containsHardBreak;
});
if (containsHardBreak) {
return null;
}
// fixes autoformatting in heading nodes: # Heading *bold*
// expected result: should not autoformat *bold*; <h1>Heading *bold*</h1>
const startPosResolved = doc.resolve(start);
const endPosResolved = doc.resolve(end);
if (startPosResolved.sameParent(endPosResolved) && !startPosResolved.parent.type.allowsMarkType(markType)) {
return null;
}
if (markType.name === 'code') {
var _api$base, _api$base$actions;
api === null || api === void 0 ? void 0 : (_api$base = api.base) === null || _api$base === void 0 ? void 0 : (_api$base$actions = _api$base.actions) === null || _api$base$actions === void 0 ? void 0 : _api$base$actions.resolveMarks(tr.mapping.map(start), tr.mapping.map(end), tr);
}
const mappedStart = tr.mapping.map(start);
const mappedEnd = tr.mapping.map(end);
tr.addMark(mappedStart, mappedEnd, markType.create());
const textSuffix = tr.doc.textBetween(mappedEnd - char.length, mappedEnd);
if (textSuffix === char) {
tr.delete(mappedEnd - char.length, mappedEnd);
}
if (textPrefix === char) {
tr.delete(mappedStart, mappedStart + char.length);
}
return tr.removeStoredMark(markType);
};
}
class ReverseRegexExp extends RegExp {
exec(str) {
if (!str) {
return null;
}
const reverseStr = [...str].reverse().join('');
const result = super.exec(reverseStr);
if (!result) {
return null;
}
for (let i = 0; i < result.length; i++) {
if (result[i] && typeof result[i] === 'string') {
result[i] = [...result[i]].reverse().join('');
}
}
if (result.input && typeof result.input === 'string') {
result.input = [...result.input].reverse().join('');
}
if (result.input && result[0]) {
result.index = result.input.length - result[0].length;
}
return result;
}
}
const buildRegex = char => {
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
const escapedChar = char.replace(/(\W)/g, '\\$1');
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp, @atlassian/perf-linting/no-expensive-split-replace -- Ignored via go/ees017 (to be fixed)
const combinations = ValidCombinations[char].map(c => c.replace(/(\W)/g, '\\$1')).join('|');
// Single X - https://regex101.com/r/McT3yq/14/
// Double X - https://regex101.com/r/pQUgjx/1/
const baseRegex = '^X(?=[^X\\s]).*?[^\\sX]X(?=[\\sOBJECT_REPLACEMENT_CHARACTER]COMBINATIONS|$)'.replace('OBJECT_REPLACEMENT_CHARACTER', leafNodeReplacementCharacter).replace('COMBINATIONS', combinations ? `|${combinations}` : '');
const replacedRegex = String.prototype.hasOwnProperty('replaceAll') ?
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
baseRegex.replaceAll('X', escapedChar) :
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
baseRegex.replace(/X/g, escapedChar);
return new ReverseRegexExp(replacedRegex);
};
const buildRegexNew = (char, allowsBackwardMatch = false) => {
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
const escapedChar = char.replace(/(\W)/g, '\\$1');
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp, @atlassian/perf-linting/no-expensive-split-replace -- Ignored via go/ees017 (to be fixed)
const combinations = ValidCombinations[char].map(c => c.replace(/(\W)/g, '\\$1')).join('|');
// Single X - https://regex101.com/r/McT3yq/14/
// Double X - https://regex101.com/r/pQUgjx/1/
// if backwards matches are allowed, do not prefix the regex with an anchor (^)
const maybeAnchor = allowsBackwardMatch ? '' : '^';
const orCombinations = combinations ? `|${combinations}` : '';
const baseRegex = `${maybeAnchor}X(?=[^X\\s]).*?[^\\sX]X(?=[\\s${leafNodeReplacementCharacter}]${orCombinations}|$)`;
const replacedRegex = String.prototype.hasOwnProperty('replaceAll') ?
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
baseRegex.replaceAll('X', escapedChar) :
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
baseRegex.replace(/X/g, escapedChar);
return new ReverseRegexExp(replacedRegex);
};
export const strongRegex1 = buildRegex(ValidAutoformatChars.STRONG);
export const strongRegex2 = buildRegex(ValidAutoformatChars.STRONG_MARKDOWN);
export const italicRegex1 = buildRegex(ValidAutoformatChars.ITALIC);
export const italicRegex2 = buildRegex(ValidAutoformatChars.ITALIC_MARKDOWN);
export const strikeRegex = buildRegex(ValidAutoformatChars.STRIKE);
export const codeRegex = buildRegex(ValidAutoformatChars.CODE);
export const codeRegexWithBackwardMatch = buildRegexNew(ValidAutoformatChars.CODE, true);
/**
* Create input rules for strong mark
*
* @param {Schema} schema
* @returns {InputRuleWrapper[]}
*/
function getStrongInputRules(schema, editorAnalyticsAPI) {
const ruleWithStrongAnalytics = inputRuleWithAnalytics({
action: ACTION.FORMATTED,
actionSubject: ACTION_SUBJECT.TEXT,
actionSubjectId: ACTION_SUBJECT_ID.FORMAT_STRONG,
eventType: EVENT_TYPE.TRACK,
attributes: {
inputMethod: INPUT_METHOD.FORMATTING
}
}, editorAnalyticsAPI);
// **string** or __strong__ should bold the text
const doubleUnderscoreRule = createRule(strongRegex1, addMark(schema.marks.strong, schema, ValidAutoformatChars.STRONG));
const doubleAsterixRule = createRule(strongRegex2, addMark(schema.marks.strong, schema, ValidAutoformatChars.STRONG_MARKDOWN));
return [ruleWithStrongAnalytics(doubleUnderscoreRule), ruleWithStrongAnalytics(doubleAsterixRule)];
}
/**
* Create input rules for em mark
*
* @param {Schema} schema
* @returns {InputRuleWrapper[]}
*/
function getItalicInputRules(schema, editorAnalyticsAPI) {
const ruleWithItalicAnalytics = inputRuleWithAnalytics({
action: ACTION.FORMATTED,
actionSubject: ACTION_SUBJECT.TEXT,
actionSubjectId: ACTION_SUBJECT_ID.FORMAT_ITALIC,
eventType: EVENT_TYPE.TRACK,
attributes: {
inputMethod: INPUT_METHOD.FORMATTING
}
}, editorAnalyticsAPI);
const underscoreRule = createRule(italicRegex1, addMark(schema.marks.em, schema, ValidAutoformatChars.ITALIC));
const asterixRule = createRule(italicRegex2, addMark(schema.marks.em, schema, ValidAutoformatChars.ITALIC_MARKDOWN));
return [ruleWithItalicAnalytics(underscoreRule), ruleWithItalicAnalytics(asterixRule)];
}
/**
* Create input rules for strike mark
*
* @param {Schema} schema
* @returns {InputRuleWrapper[]}
*/
function getStrikeInputRules(schema, editorAnalyticsAPI) {
const ruleWithStrikeAnalytics = inputRuleWithAnalytics({
action: ACTION.FORMATTED,
actionSubject: ACTION_SUBJECT.TEXT,
actionSubjectId: ACTION_SUBJECT_ID.FORMAT_STRIKE,
eventType: EVENT_TYPE.TRACK,
attributes: {
inputMethod: INPUT_METHOD.FORMATTING
}
}, editorAnalyticsAPI);
const doubleTildeRule = createRule(strikeRegex, addMark(schema.marks.strike, schema, ValidAutoformatChars.STRIKE));
return [ruleWithStrikeAnalytics(doubleTildeRule)];
}
/**
* Create input rules for code mark
*
* @param {Schema} schema
* @returns {InputRuleWrapper[]}
*/
function getCodeInputRules(schema, editorAnalyticsAPI, api) {
const ruleWithCodeAnalytics = inputRuleWithAnalytics({
action: ACTION.FORMATTED,
actionSubject: ACTION_SUBJECT.TEXT,
actionSubjectId: ACTION_SUBJECT_ID.FORMAT_CODE,
eventType: EVENT_TYPE.TRACK,
attributes: {
inputMethod: INPUT_METHOD.FORMATTING
}
}, editorAnalyticsAPI);
const allowsBackwardMatch = expValEquals('platform_editor_lovability_inline_code', 'isEnabled', true);
const backTickRule = createRule(allowsBackwardMatch ? codeRegexWithBackwardMatch : codeRegex, addMark(schema.marks.code, schema, ValidAutoformatChars.CODE, api), allowsBackwardMatch);
return [ruleWithCodeAnalytics(backTickRule)];
}
export function inputRulePlugin(schema, editorAnalyticsAPI, api) {
const rules = [];
if (schema.marks.strong) {
rules.push(...getStrongInputRules(schema, editorAnalyticsAPI));
}
if (schema.marks.em) {
rules.push(...getItalicInputRules(schema, editorAnalyticsAPI));
}
if (schema.marks.strike) {
rules.push(...getStrikeInputRules(schema, editorAnalyticsAPI));
}
if (schema.marks.code) {
rules.push(...getCodeInputRules(schema, editorAnalyticsAPI, api));
}
if (rules.length !== 0) {
return new SafePlugin(createPlugin('text-formatting', rules));
}
return;
}
export default inputRulePlugin;