UNPKG

@atlaskit/editor-plugin-local-id

Version:

LocalId plugin for @atlaskit/editor-core

512 lines (486 loc) 20.1 kB
import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } import { BatchAttrsStep, SetAttrsStep } from '@atlaskit/adf-schema/steps'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { PluginKey } from '@atlaskit/editor-prosemirror/state'; import { AttrStep, DocAttrStep, ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform'; /** * This is a safeguard limit to avoid tracking localIds in extremely large documents with too many localIds. * If the number of unique localIds exceeds this limit, the watchmen plugin will disable itself to avoid performance issues. * Reminder: The Map has a hard limit of 2^24 (16 million) entries in V8, please keep this value well below that * to avoid any potential memory issues. */ var MAX_LOCAL_ID_MAP_SIZE = 2097152; // 2^21 /** * Plugin state tracking all localIds in the document */ export var localIdWatchmenPluginKey = new PluginKey('localIdWatchmenPlugin'); /** * Scans the entire document to find all active localIds */ var scanDocumentForLocalIds = function scanDocumentForLocalIds(doc) { var localIds = new Set(); doc.descendants(function (node) { var _node$attrs; if ((_node$attrs = node.attrs) !== null && _node$attrs !== void 0 && _node$attrs.localId) { localIds.add(node.attrs.localId); } // Check marks for localIds if (node.marks) { node.marks.forEach(function (mark) { var _mark$attrs; if ((_mark$attrs = mark.attrs) !== null && _mark$attrs !== void 0 && _mark$attrs.localId) { localIds.add(mark.attrs.localId); } }); } return localIds.size < MAX_LOCAL_ID_MAP_SIZE; // Continue traversing }); return localIds; }; var getReplacementStatusCode = function getReplacementStatusCode(tr, step) { var method; if (step instanceof AttrStep || step instanceof DocAttrStep) { method = 'ByAttr'; } else if (step instanceof SetAttrsStep) { method = 'BySetAttrs'; } else if (step instanceof BatchAttrsStep) { method = 'ByBatchAttrs'; } else if (step instanceof ReplaceStep) { var isDeleting = step.from < step.to; // range has content to remove var isInserting = step.slice.content.size > 0; // slice has content to insert if (isDeleting && !isInserting) { method = 'ByDelete'; // removing content, inserting nothing //} else if (!isDeleting && isInserting) { //method = 'ByInsert'; // This situation cannot be tracked since this would be part of the "current" status } else { // isDeleting && isInserting method = 'ByReplace'; } } else if (step instanceof ReplaceAroundStep) { method = 'ByReplaceAround'; } else { method = 'ByUnknown'; } if (tr.getMeta('isAIStreamingTransformation')) { return "AIChange".concat(method); } if (tr.getMeta('replaceDocument')) { return "docChange".concat(method); } if (tr.getMeta('isRemote')) { return "remoteChange".concat(method); } return "localChange".concat(method); }; /** * Handles AttrStep and DocAttrStep which modify a single attribute */ var handleAttrStep = function handleAttrStep(tr, step, localIdStatus, preDoc) { if (step.attr !== 'localId') { return { localIdStatus: localIdStatus, modified: false }; } var modified = false; var newlocalIdStatus = new Map(localIdStatus); // Get the old value if it exists var oldLocalId; if (step instanceof AttrStep) { try { var _node$attrs2; var node = preDoc.nodeAt(step.pos); oldLocalId = node === null || node === void 0 || (_node$attrs2 = node.attrs) === null || _node$attrs2 === void 0 ? void 0 : _node$attrs2.localId; } catch (_unused) { // Position might be invalid } } // Handle the new value var newLocalId = step.value; if (oldLocalId && oldLocalId !== newLocalId) { // Old localId is being replaced or removed newlocalIdStatus.set(oldLocalId, getReplacementStatusCode(tr, step)); modified = true; } if (newLocalId) { newlocalIdStatus.set(newLocalId, 'current'); modified = true; } return { localIdStatus: newlocalIdStatus, modified: modified }; }; /** * Handles SetAttrsStep which sets multiple attributes at once */ var handleSetAttrsStep = function handleSetAttrsStep(tr, step, localIdStatus, preDoc) { var attrs = step.attrs; if (!attrs || !attrs.hasOwnProperty('localId')) { return { localIdStatus: localIdStatus, modified: false }; } var modified = false; var newlocalIdStatus = new Map(localIdStatus); // Get old localId from the node being modified try { var _node$attrs3; var node = preDoc.nodeAt(step.pos); var oldLocalId = node === null || node === void 0 || (_node$attrs3 = node.attrs) === null || _node$attrs3 === void 0 ? void 0 : _node$attrs3.localId; if (oldLocalId && oldLocalId !== attrs.localId) { newlocalIdStatus.set(oldLocalId, getReplacementStatusCode(tr, step)); modified = true; } } catch (_unused2) { // Position might be invalid } var newLocalId = attrs.localId; if (newLocalId) { newlocalIdStatus.set(newLocalId, 'current'); modified = true; } return { localIdStatus: newlocalIdStatus, modified: modified }; }; /** * Handles BatchAttrsStep which applies multiple attribute changes */ var handleBatchAttrsStep = function handleBatchAttrsStep(tr, step, localIdStatus, preDoc) { var modified = false; var newlocalIdStatus = new Map(localIdStatus); step.data.forEach(function (change) { var _change$attrs; if (!((_change$attrs = change.attrs) !== null && _change$attrs !== void 0 && _change$attrs.hasOwnProperty('localId'))) { return; } // Get old localId from the node being modified try { var _node$attrs4; var node = preDoc.nodeAt(change.position); var oldLocalId = node === null || node === void 0 || (_node$attrs4 = node.attrs) === null || _node$attrs4 === void 0 ? void 0 : _node$attrs4.localId; var newLocalId = change.attrs.localId; if (oldLocalId && oldLocalId !== newLocalId) { newlocalIdStatus.set(oldLocalId, getReplacementStatusCode(tr, step)); modified = true; } if (newLocalId) { newlocalIdStatus.set(newLocalId, 'current'); modified = true; } } catch (_unused3) { // Position might be invalid } }); return { localIdStatus: newlocalIdStatus, modified: modified }; }; /** * Handles ReplaceStep which inserts or deletes content */ var handleReplaceStep = function handleReplaceStep(tr, step, localIdStatus, preDoc, postDoc) { var modified = false; try { // Create a temporary set to collect new localIds var changedLocaleIds = new Map(); var replaceCode = getReplacementStatusCode(tr, step); step.getMap().forEach(function (oldStart, oldEnd, newStart, newEnd) { // For each step map item we can just look at the nodes in the old doc and mark them as "inactive" and then // look at the nodes in the doc after the step has been applied and mark them as "active" // then lastly we can compare these to the current states and only update what's different. preDoc.nodesBetween(oldStart, oldEnd, function (node) { var _node$attrs5; if ((_node$attrs5 = node.attrs) !== null && _node$attrs5 !== void 0 && _node$attrs5.localId) { changedLocaleIds.set(node.attrs.localId, replaceCode); } if (node.marks) { node.marks.forEach(function (mark) { var _mark$attrs2; if ((_mark$attrs2 = mark.attrs) !== null && _mark$attrs2 !== void 0 && _mark$attrs2.localId) { changedLocaleIds.set(node.attrs.localId, replaceCode); } }); } }); postDoc.nodesBetween(newStart, newEnd, function (node) { var _node$attrs6; if ((_node$attrs6 = node.attrs) !== null && _node$attrs6 !== void 0 && _node$attrs6.localId) { changedLocaleIds.set(node.attrs.localId, 'current'); } if (node.marks) { node.marks.forEach(function (mark) { var _mark$attrs3; if ((_mark$attrs3 = mark.attrs) !== null && _mark$attrs3 !== void 0 && _mark$attrs3.localId) { changedLocaleIds.set(node.attrs.localId, 'current'); } }); } }); }); if (!!changedLocaleIds.size) { var newlocalIdStatus = new Map(localIdStatus); var _iterator = _createForOfIteratorHelper(changedLocaleIds), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var _step$value = _slicedToArray(_step.value, 2), key = _step$value[0], value = _step$value[1]; if (!localIdStatus.has(key) || localIdStatus.get(key) !== value) { modified = true; newlocalIdStatus.set(key, value); } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return { localIdStatus: newlocalIdStatus, modified: modified }; } } catch (_unused4) { // If position calculation fails, do a full document rescan as fallback // This shouldn't happen often but provides safety } return { localIdStatus: localIdStatus, modified: modified }; }; /** * Handles ReplaceAroundStep which wraps or unwraps content */ var handleReplaceAroundStep = function handleReplaceAroundStep(tr, step, localIdStatus, preDoc, postDoc) { var modified = false; var newlocalIdStatus = new Map(localIdStatus); // Scan the affected region before and after the step var from = step.from; var to = step.to; try { // Collect localIds from the old region var oldLocalIds = new Set(); if (from < to && from >= 0 && to <= preDoc.content.size) { preDoc.nodesBetween(from, to, function (node) { var _node$attrs7; if ((_node$attrs7 = node.attrs) !== null && _node$attrs7 !== void 0 && _node$attrs7.localId) { oldLocalIds.add(node.attrs.localId); } if (node.marks) { node.marks.forEach(function (mark) { var _mark$attrs4; if ((_mark$attrs4 = mark.attrs) !== null && _mark$attrs4 !== void 0 && _mark$attrs4.localId) { oldLocalIds.add(mark.attrs.localId); } }); } }); } // Collect localIds from the new region var map = step.getMap(); var newFrom = map.map(from, -1); var newTo = map.map(to, 1); var newLocalIds = new Set(); if (newFrom < newTo && newFrom >= 0 && newTo <= postDoc.content.size) { postDoc.nodesBetween(newFrom, newTo, function (node) { var _node$attrs8; if ((_node$attrs8 = node.attrs) !== null && _node$attrs8 !== void 0 && _node$attrs8.localId) { newLocalIds.add(node.attrs.localId); } if (node.marks) { node.marks.forEach(function (mark) { var _mark$attrs5; if ((_mark$attrs5 = mark.attrs) !== null && _mark$attrs5 !== void 0 && _mark$attrs5.localId) { newLocalIds.add(mark.attrs.localId); } }); } }); } // Find localIds that were removed oldLocalIds.forEach(function (localId) { if (!newLocalIds.has(localId) && newlocalIdStatus.get(localId) === 'current') { newlocalIdStatus.set(localId, getReplacementStatusCode(tr, step)); modified = true; } }); // Find localIds that were added newLocalIds.forEach(function (localId) { if (!oldLocalIds.has(localId)) { newlocalIdStatus.set(localId, 'current'); modified = true; } }); } catch (_unused5) { // Position might be invalid, skip this step } return { localIdStatus: newlocalIdStatus, modified: modified }; }; /** * Processes a transaction to update localId tracking state */ var processTransaction = function processTransaction(tr, currentState) { var localIdStatus = currentState.localIdStatus; var modified = false; // Process each step in the transaction try { tr.steps.forEach(function (step, index) { var _tr$docs$index, _tr$docs, _tr$docs2, _tr$docs3; var result; // steps are relative to their docs, so we ensure we reference the doc before/after the step was applied. var preDoc = (_tr$docs$index = (_tr$docs = tr.docs) === null || _tr$docs === void 0 ? void 0 : _tr$docs[index]) !== null && _tr$docs$index !== void 0 ? _tr$docs$index : tr.doc; var postDoc = (_tr$docs2 = (_tr$docs3 = tr.docs) === null || _tr$docs3 === void 0 ? void 0 : _tr$docs3[index + 1]) !== null && _tr$docs2 !== void 0 ? _tr$docs2 : tr.doc; if (step instanceof AttrStep || step instanceof DocAttrStep) { result = handleAttrStep(tr, step, localIdStatus, preDoc); } else if (step instanceof SetAttrsStep) { result = handleSetAttrsStep(tr, step, localIdStatus, preDoc); } else if (step instanceof BatchAttrsStep) { result = handleBatchAttrsStep(tr, step, localIdStatus, preDoc); } else if (step instanceof ReplaceStep) { result = handleReplaceStep(tr, step, localIdStatus, preDoc, postDoc); } else if (step instanceof ReplaceAroundStep) { result = handleReplaceAroundStep(tr, step, localIdStatus, preDoc, postDoc); } else { // Unknown step type, no changes result = { localIdStatus: localIdStatus, modified: false }; } localIdStatus = result.localIdStatus; modified = modified || result.modified; }); } catch (_unused6) { // If any error occurs during step processing, we fallback to disabling the plugin return { enabled: false, initLocalIdSize: currentState.initLocalIdSize, localIdStatus: new Map(), lastUpdated: Date.now() }; } // If nothing changed, return the same state object if (!modified) { return currentState; } // If we exceeded the max size while processing the steps, we need to disable the watchmen from further processing. // If a Map size limit of 2^24 is exceeded then it's more than likely an error would have been thrown during processing // which would also disable this plugin. if (localIdStatus.size >= MAX_LOCAL_ID_MAP_SIZE) { return { enabled: false, initLocalIdSize: currentState.initLocalIdSize, localIdStatus: new Map(), lastUpdated: Date.now() }; } // Return new state with updated sets return { enabled: true, initLocalIdSize: currentState.initLocalIdSize, localIdStatus: localIdStatus, lastUpdated: Date.now() }; }; /** * Creates the localId watchmen plugin */ export var createWatchmenPlugin = function createWatchmenPlugin(api) { // Ensure limited mode is initialized return new SafePlugin({ key: localIdWatchmenPluginKey, state: { init: function init(_config, state) { var _api$limitedMode$shar, _api$limitedMode; var isLimitedModeEnabled = (_api$limitedMode$shar = api === null || api === void 0 || (_api$limitedMode = api.limitedMode) === null || _api$limitedMode === void 0 || (_api$limitedMode = _api$limitedMode.sharedState.currentState()) === null || _api$limitedMode === void 0 ? void 0 : _api$limitedMode.enabled) !== null && _api$limitedMode$shar !== void 0 ? _api$limitedMode$shar : false; if (isLimitedModeEnabled) { return { enabled: false, initLocalIdSize: -1, localIdStatus: new Map(), lastUpdated: Date.now() }; } // Initialize by scanning the entire document var activeLocalIds = scanDocumentForLocalIds(state.doc); if (activeLocalIds.size >= MAX_LOCAL_ID_MAP_SIZE) { return { enabled: false, initLocalIdSize: activeLocalIds.size, localIdStatus: new Map(), lastUpdated: Date.now() }; } return { enabled: true, initLocalIdSize: activeLocalIds.size, localIdStatus: new Map(Array.from(activeLocalIds).map(function (key) { return [key, 'current']; })), lastUpdated: Date.now() }; }, apply: function apply(tr, currentPluginState) { var _ref = tr.getMeta(localIdWatchmenPluginKey) || { enabled: currentPluginState.enabled }, enabled = _ref.enabled; var newPluginState = currentPluginState; if (enabled !== currentPluginState.enabled) { // If this plugin enabled state is changing and it's being disabled at runtime then we will kill this plugin // to avoid tracking localIds when in limited mode or there after. // Once disabled it cannot be re-enabled without a full editor reload. if (!enabled) { return { enabled: false, initLocalIdSize: currentPluginState.initLocalIdSize, localIdStatus: currentPluginState.localIdStatus, lastUpdated: Date.now() }; } } if (!newPluginState.enabled) { // If this plugin has been disabled, do not track localIds. return newPluginState; } // If no steps, nothing changed if (tr.steps.length === 0 || !tr.docChanged) { return newPluginState; } // Process the transaction to update state return processTransaction(tr, newPluginState); } }, view: function view(editorView) { var _api$limitedMode2; // If limited mode changes, for example if we start not limited but then all of a sudden become limited, we kill // the watchment plugin to avoid tracking localIds when in limited mode. We also don't want/need to re-enable it once it's disabled. var unsub = api === null || api === void 0 || (_api$limitedMode2 = api.limitedMode) === null || _api$limitedMode2 === void 0 ? void 0 : _api$limitedMode2.sharedState.onChange(function (_ref2) { var nextSharedState = _ref2.nextSharedState; var watchmentPluginState = localIdWatchmenPluginKey.getState(editorView.state); if (nextSharedState.enabled && (watchmentPluginState === null || watchmentPluginState === void 0 ? void 0 : watchmentPluginState.enabled) === true) { // if nextSharedState.enabled === true, then we need to disable the watchmen plugin, if not already disabled editorView.dispatch(editorView.state.tr.setMeta(localIdWatchmenPluginKey, { enabled: false })); } }); return { destroy: unsub }; } }); };