@atlaskit/editor-plugin-local-id
Version:
LocalId plugin for @atlaskit/editor-core
519 lines (492 loc) • 20.5 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.localIdWatchmenPluginKey = exports.createWatchmenPlugin = void 0;
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _steps = require("@atlaskit/adf-schema/steps");
var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
var _state = require("@atlaskit/editor-prosemirror/state");
var _transform = require("@atlaskit/editor-prosemirror/transform");
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; }
/**
* 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
*/
var localIdWatchmenPluginKey = exports.localIdWatchmenPluginKey = new _state.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 _transform.AttrStep || step instanceof _transform.DocAttrStep) {
method = 'ByAttr';
} else if (step instanceof _steps.SetAttrsStep) {
method = 'BySetAttrs';
} else if (step instanceof _steps.BatchAttrsStep) {
method = 'ByBatchAttrs';
} else if (step instanceof _transform.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 _transform.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 _transform.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 = (0, _slicedToArray2.default)(_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 _transform.AttrStep || step instanceof _transform.DocAttrStep) {
result = handleAttrStep(tr, step, localIdStatus, preDoc);
} else if (step instanceof _steps.SetAttrsStep) {
result = handleSetAttrsStep(tr, step, localIdStatus, preDoc);
} else if (step instanceof _steps.BatchAttrsStep) {
result = handleBatchAttrsStep(tr, step, localIdStatus, preDoc);
} else if (step instanceof _transform.ReplaceStep) {
result = handleReplaceStep(tr, step, localIdStatus, preDoc, postDoc);
} else if (step instanceof _transform.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
*/
var createWatchmenPlugin = exports.createWatchmenPlugin = function createWatchmenPlugin(api) {
// Ensure limited mode is initialized
return new _safePlugin.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
};
}
});
};