@atlaskit/editor-plugin-code-block
Version:
Code block plugin for @atlaskit/editor-core
190 lines (187 loc) • 9.36 kB
JavaScript
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import _typeof from "@babel/runtime/helpers/typeof";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import { getInsertedCodeBlocksInTransaction } from '@atlaskit/editor-common/code-block';
import { pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
import { getAllChangedCodeBlocksInTransaction } from '../pm-plugins/utils';
var MIN_AUTO_DETECT_TEXT_LENGTH = 20;
export var shouldTriggerLargeChangeDetection = function shouldTriggerLargeChangeDetection(lastObservedText, text) {
if (!lastObservedText) {
return text.length > 0;
}
return Math.abs(text.length - lastObservedText.length) > lastObservedText.length / 2;
};
export var getFirstLine = function getFirstLine(text) {
var _text$split$;
return (_text$split$ = text.split('\n')[0]) !== null && _text$split$ !== void 0 ? _text$split$ : '';
};
export var hasEnoughTextForAutoDetection = function hasEnoughTextForAutoDetection(text) {
return text.trim().length >= MIN_AUTO_DETECT_TEXT_LENGTH;
};
export var getLocalId = function getLocalId(node) {
return typeof node.attrs.localId === 'string' ? node.attrs.localId : null;
};
export var createAutoDetectEntry = function createAutoDetectEntry(node, pos, isPending, previous) {
var options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {};
return {
lastObservedText: node.textContent,
lastObservedFirstLine: getFirstLine(node.textContent),
isPending: isPending,
detectionResult: options.preserveDetectionResult === false ? undefined : previous === null || previous === void 0 ? void 0 : previous.detectionResult,
autoDetectedLanguage: previous === null || previous === void 0 ? void 0 : previous.autoDetectedLanguage,
pos: pos
};
};
export var queueAutoDetection = function queueAutoDetection(languageDetectionMap, node, pos, isPending) {
var localId = getLocalId(node);
if (!localId) {
return languageDetectionMap;
}
return _objectSpread(_objectSpread({}, languageDetectionMap), {}, _defineProperty({}, localId, createAutoDetectEntry(node, pos, isPending, languageDetectionMap[localId])));
};
export var removeAutoDetection = function removeAutoDetection(languageDetectionMap, localId) {
if (!languageDetectionMap[localId]) {
return languageDetectionMap;
}
var nextLanguageDetectionMap = _objectSpread({}, languageDetectionMap);
delete nextLanguageDetectionMap[localId];
return nextLanguageDetectionMap;
};
var getCodeBlockLocalIdsRemovedFromChangedRanges = function getCodeBlockLocalIdsRemovedFromChangedRanges(tr, codeBlockType) {
var localIds = new Set();
tr.steps.forEach(function (step, stepIndex) {
var docAtStep = tr.docs[stepIndex];
step.getMap().forEach(function (oldStart, oldEnd) {
if (oldStart === oldEnd) {
return;
}
var clampedOldEnd = Math.min(oldEnd, docAtStep.content.size);
docAtStep.nodesBetween(oldStart, clampedOldEnd, function (node, pos) {
if (node.type !== codeBlockType) {
return true;
}
var isWholeCodeBlockRemoved = pos >= oldStart && pos + node.nodeSize <= clampedOldEnd;
if (!isWholeCodeBlockRemoved) {
return false;
}
var localId = getLocalId(node);
if (localId) {
localIds.add(localId);
}
return false;
});
});
});
return localIds;
};
var isHistoryMeta = function isHistoryMeta(meta) {
return _typeof(meta) === 'object' && meta !== null && 'redo' in meta;
};
var getCodeBlockTransactionChanges = function getCodeBlockTransactionChanges(tr, codeBlockType) {
var insertedNodesWithPos = getInsertedCodeBlocksInTransaction(tr, codeBlockType);
var removedFromChangedRangesLocalIds = getCodeBlockLocalIdsRemovedFromChangedRanges(tr, codeBlockType);
var insertedLocalIds = new Set();
var insertedCodeBlocks = [];
insertedNodesWithPos.forEach(function (_ref) {
var node = _ref.node,
pos = _ref.pos;
var localId = getLocalId(node);
if (!localId) {
return;
}
insertedLocalIds.add(localId);
if (!removedFromChangedRangesLocalIds.has(localId)) {
insertedCodeBlocks.push({
localId: localId,
node: node,
pos: pos
});
}
});
var deletedLocalIds = new Set();
removedFromChangedRangesLocalIds.forEach(function (localId) {
if (!insertedLocalIds.has(localId)) {
deletedLocalIds.add(localId);
}
});
return {
deletedLocalIds: deletedLocalIds,
insertedCodeBlocks: insertedCodeBlocks
};
};
export var updateAutoDetectState = function updateAutoDetectState(tr, pluginState) {
var codeBlock = tr.doc.type.schema.nodes.codeBlock;
var isPaste = tr.getMeta('paste') === true || tr.getMeta('uiEvent') === 'paste';
var isExternalContentChange = tr.getMeta('replaceDocument') === true || tr.getMeta('isRemote') === true;
var historyMeta = tr.getMeta(pmHistoryPluginKey);
var isUndo = isHistoryMeta(historyMeta) && historyMeta.redo === false;
var hasTrackedEntries = Object.keys(pluginState.languageDetectionMap).length > 0;
// Page loads and remote edits should not start auto-detection for code blocks.
if (!hasTrackedEntries && isExternalContentChange) {
return pluginState.languageDetectionMap;
}
// Existing entries still need mapping/deletion cleanup, but external edits should not refresh text.
var changedCodeBlockNodes = isExternalContentChange ? [] : getAllChangedCodeBlocksInTransaction(tr);
if (!hasTrackedEntries && !changedCodeBlockNodes.length) {
return pluginState.languageDetectionMap;
}
var _getCodeBlockTransact = getCodeBlockTransactionChanges(tr, codeBlock),
deletedLocalIds = _getCodeBlockTransact.deletedLocalIds,
insertedCodeBlocks = _getCodeBlockTransact.insertedCodeBlocks;
var languageDetectionMap = hasTrackedEntries ? Object.fromEntries(Object.entries(pluginState.languageDetectionMap).map(function (_ref2) {
var _ref3 = _slicedToArray(_ref2, 2),
localId = _ref3[0],
entry = _ref3[1];
return [localId, _objectSpread(_objectSpread({}, entry), {}, {
pos: tr.mapping.map(entry.pos)
})];
})) : pluginState.languageDetectionMap;
if (!isExternalContentChange) {
insertedCodeBlocks.forEach(function (_ref4) {
var localId = _ref4.localId,
node = _ref4.node,
pos = _ref4.pos;
if (node.attrs.language) {
languageDetectionMap = removeAutoDetection(languageDetectionMap, localId);
return;
}
languageDetectionMap = queueAutoDetection(languageDetectionMap, node, pos, hasEnoughTextForAutoDetection(node.textContent));
});
changedCodeBlockNodes.forEach(function (_ref5) {
var _tr$before$nodeAt;
var node = _ref5.node,
pos = _ref5.pos;
var localId = getLocalId(node);
if (!localId || !languageDetectionMap[localId]) {
return;
}
var currentLanguage = node.attrs.language;
var previousEntry = languageDetectionMap[localId];
// Undo metadata does not identify the changed node; compare the pre/post language
// so text undo inside auto-detection mode keeps the entry.
var previousLanguage = isUndo ? (_tr$before$nodeAt = tr.before.nodeAt(tr.mapping.invert().map(pos))) === null || _tr$before$nodeAt === void 0 ? void 0 : _tr$before$nodeAt.attrs.language : undefined;
if (isUndo && previousLanguage && !currentLanguage || currentLanguage && currentLanguage !== previousEntry.autoDetectedLanguage) {
languageDetectionMap = removeAutoDetection(languageDetectionMap, localId);
return;
}
var text = node.textContent;
var firstLine = getFirstLine(text);
var shouldTriggerDetection = previousEntry.isPending || isPaste || firstLine !== previousEntry.lastObservedFirstLine || shouldTriggerLargeChangeDetection(previousEntry.lastObservedText, text);
var isPending = hasEnoughTextForAutoDetection(text) && shouldTriggerDetection;
// Only pending detection refreshes the text snapshot; otherwise gradual typing
// should continue comparing against the last attempted detection.
languageDetectionMap = isPending ? queueAutoDetection(languageDetectionMap, node, pos, true) : _objectSpread(_objectSpread({}, languageDetectionMap), {}, _defineProperty({}, localId, _objectSpread(_objectSpread({}, previousEntry), {}, {
isPending: false,
pos: pos
})));
});
}
deletedLocalIds.forEach(function (localId) {
if (languageDetectionMap[localId]) {
languageDetectionMap = removeAutoDetection(languageDetectionMap, localId);
}
});
return languageDetectionMap;
};