@lobehub/editor
Version:
A powerful and extensible rich text editor built on Meta's Lexical framework, providing a modern editing experience with React integration.
736 lines (702 loc) • 29.9 kB
JavaScript
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, 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 normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { $getEndOfCodeInLine, $getFirstCodeNodeOfLine, $getLastCodeNodeOfLine, $getStartOfCodeInLine, $isCodeHighlightNode, $isCodeNode, CodeHighlightNode, CodeNode, DEFAULT_CODE_LANGUAGE } from '@lexical/code';
import { mergeRegister } from '@lexical/utils';
import { $createLineBreakNode, $createPoint, $createTabNode, $createTextNode, $getCaretRange, $getCaretRangeInDirection, $getNodeByKey, $getSelection, $getSiblingCaret, $getTextPointCaret, $insertNodes, $isLineBreakNode, $isRangeSelection, $isTabNode, $isTextNode, $normalizeCaret, $setSelectionFromCaretRange, COMMAND_PRIORITY_LOW, INDENT_CONTENT_COMMAND, INSERT_TAB_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_TAB_COMMAND, MOVE_TO_END, MOVE_TO_START, OUTDENT_CONTENT_COMMAND, TextNode } from 'lexical';
import { $getHighlightNodes, getHighlightSerializeNode, isCodeLanguageLoaded, isCodeThemeLoaded, loadCodeLanguage, loadCodeTheme } from "./FacadeShiki";
import invariant from "./invariant";
var DEFAULT_CODE_THEME = 'lobe-theme';
export var ShikiTokenizer = {
$tokenize: function $tokenize(codeNode, language) {
return $getHighlightNodes(codeNode, language || this.defaultLanguage);
},
$tokenizeSerialized: function $tokenizeSerialized(code, language, theme) {
return getHighlightSerializeNode(code, language || this.defaultLanguage,
// eslint-disable-next-line @typescript-eslint/no-use-before-define
toCodeTheme({
defaultTheme: theme || this.defaultTheme
}));
},
defaultLanguage: DEFAULT_CODE_LANGUAGE,
defaultTheme: DEFAULT_CODE_THEME
};
export function toCodeTheme(tokenizer) {
if (typeof tokenizer.defaultTheme === 'string') {
return tokenizer.defaultTheme;
}
return tokenizer.defaultTheme.light + ' ' + tokenizer.defaultTheme.dark;
}
function $textNodeTransform(node, editor, tokenizer) {
// Since CodeNode has flat children structure we only need to check
// if node's parent is a code node and run highlighting if so
var parentNode = node.getParent();
if ($isCodeNode(parentNode)) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
codeNodeTransform(parentNode, editor, tokenizer);
} else if ($isCodeHighlightNode(node)) {
// When code block converted into paragraph or other element
// code highlight nodes converted back to normal text
node.replace($createTextNode(node.__text));
}
}
function updateCodeGutter(node, editor) {
var codeElement = editor.getElementByKey(node.getKey());
if (codeElement === null) {
return;
}
var children = node.getChildren();
var childrenLength = children.length;
// @ts-ignore: internal field
if (childrenLength === codeElement.__cachedChildrenLength) {
// Avoid updating the attribute if the children length hasn't changed.
return;
}
// @ts-ignore:: internal field
codeElement.__cachedChildrenLength = childrenLength;
var gutter = '1';
var count = 1;
for (var i = 0; i < childrenLength; i++) {
if ($isLineBreakNode(children[i])) {
gutter += '\n' + ++count;
}
}
codeElement.dataset.gutter = gutter;
}
// Using `skipTransforms` to prevent extra transforms since reformatting the code
// will not affect code block content itself.
//
// Using extra cache (`nodesCurrentlyHighlighting`) since both CodeNode and CodeHighlightNode
// transforms might be called at the same time (e.g. new CodeHighlight node inserted) and
// in both cases we'll rerun whole reformatting over CodeNode, which is redundant.
// Especially when pasting code into CodeBlock.
var MAX_CONCURRENT_HIGHLIGHTING = 2;
var nodesCurrentlyHighlighting = new Set();
var waitingNodesCurrentlyHighlighting = new Set();
function codeNodeTransform(node, editor, tokenizer) {
var nodeKey = node.getKey();
// When new code block inserted it might not have language selected
var language = node.getLanguage();
if (!language) {
language = tokenizer.defaultLanguage;
node.setLanguage(language);
}
var theme = node.getTheme();
if (!theme) {
theme = toCodeTheme(tokenizer);
node.setTheme(theme);
}
// dynamic import of themes
var inFlight = false;
if (!isCodeThemeLoaded(theme)) {
loadCodeTheme(theme, editor, nodeKey);
inFlight = true;
}
// dynamic import of languages
if (isCodeLanguageLoaded(language)) {
if (!node.getIsSyntaxHighlightSupported()) {
node.setIsSyntaxHighlightSupported(true);
}
} else {
if (node.getIsSyntaxHighlightSupported()) {
node.setIsSyntaxHighlightSupported(false);
}
loadCodeLanguage(language, editor, nodeKey);
inFlight = true;
}
if (inFlight) {
return;
}
if (nodesCurrentlyHighlighting.has(nodeKey)) {
return;
}
if (nodesCurrentlyHighlighting.size > MAX_CONCURRENT_HIGHLIGHTING) {
waitingNodesCurrentlyHighlighting.add(nodeKey);
return;
}
nodesCurrentlyHighlighting.add(nodeKey);
// Using nested update call to pass `skipTransforms` since we don't want
// each individual CodeHighlightNode to be transformed again as it's already
// in its final state
editor.update(function () {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
$updateAndRetainSelection(nodeKey, function () {
var currentNode = $getNodeByKey(nodeKey);
if (!$isCodeNode(currentNode) || !currentNode.isAttached()) {
return false;
}
var lang = currentNode.getLanguage() || tokenizer.defaultLanguage;
var highlightNodes = tokenizer.$tokenize(currentNode, lang);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
var diffRange = getDiffRange(currentNode.getChildren(), highlightNodes);
var from = diffRange.from,
to = diffRange.to,
nodesForReplacement = diffRange.nodesForReplacement;
if (from !== to || nodesForReplacement.length) {
node.splice(from, to - from, nodesForReplacement);
return true;
}
return false;
});
}, {
onUpdate: function onUpdate() {
nodesCurrentlyHighlighting.delete(nodeKey);
if (nodesCurrentlyHighlighting.size < MAX_CONCURRENT_HIGHLIGHTING && waitingNodesCurrentlyHighlighting.size) {
var next = waitingNodesCurrentlyHighlighting.values().next().value;
if (!next) return;
waitingNodesCurrentlyHighlighting.delete(next);
requestAnimationFrame(function () {
editor.getEditorState().read(function () {
codeNodeTransform($getNodeByKey(next), editor, tokenizer);
});
});
}
},
skipTransforms: true
});
}
// Wrapping update function into selection retainer, that tries to keep cursor at the same
// position as before.
function $updateAndRetainSelection(nodeKey, updateFn) {
var node = $getNodeByKey(nodeKey);
if (!$isCodeNode(node) || !node.isAttached()) {
return;
}
var selection = $getSelection();
// If it's not range selection (or null selection) there's no need to change it,
// but we can still run highlighting logic
if (!$isRangeSelection(selection)) {
updateFn();
return;
}
var anchor = selection.anchor;
var anchorOffset = anchor.offset;
var isNewLineAnchor = anchor.type === 'element' && $isLineBreakNode(node.getChildAtIndex(anchor.offset - 1));
var textOffset = 0;
// Calculating previous text offset (all text node prior to anchor + anchor own text offset)
if (!isNewLineAnchor) {
var anchorNode = anchor.getNode();
textOffset = anchorOffset + anchorNode.getPreviousSiblings().reduce(function (offset, _node) {
return offset + _node.getTextContentSize();
}, 0);
}
var hasChanges = updateFn();
if (!hasChanges) {
return;
}
// Non-text anchors only happen for line breaks, otherwise
// selection will be within text node (code highlight node)
if (isNewLineAnchor) {
anchor.getNode().select(anchorOffset, anchorOffset);
return;
}
// If it was non-element anchor then we walk through child nodes
// and looking for a position of original text offset
node.getChildren().some(function (_node) {
var isText = $isTextNode(_node);
if (isText || $isLineBreakNode(_node)) {
var textContentSize = _node.getTextContentSize();
if (isText && textContentSize >= textOffset) {
_node.select(textOffset, textOffset);
return true;
}
textOffset -= textContentSize;
}
return false;
});
}
// Finds minimal diff range between two nodes lists. It returns from/to range boundaries of prevNodes
// that needs to be replaced with `nodes` (subset of nextNodes) to make prevNodes equal to nextNodes.
function getDiffRange(prevNodes, nextNodes) {
var leadingMatch = 0;
while (leadingMatch < prevNodes.length) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if (!isEqual(prevNodes[leadingMatch], nextNodes[leadingMatch])) {
break;
}
leadingMatch++;
}
var prevNodesLength = prevNodes.length;
var nextNodesLength = nextNodes.length;
var maxTrailingMatch = Math.min(prevNodesLength, nextNodesLength) - leadingMatch;
var trailingMatch = 0;
while (trailingMatch < maxTrailingMatch) {
trailingMatch++;
if (
// eslint-disable-next-line @typescript-eslint/no-use-before-define
!isEqual(prevNodes[prevNodesLength - trailingMatch], nextNodes[nextNodesLength - trailingMatch])) {
trailingMatch--;
break;
}
}
var from = leadingMatch;
var to = prevNodesLength - trailingMatch;
var nodesForReplacement = nextNodes.slice(leadingMatch, nextNodesLength - trailingMatch);
return {
from: from,
nodesForReplacement: nodesForReplacement,
to: to
};
}
function isEqual(nodeA, nodeB) {
// Only checking for code highlight nodes, tabs and linebreaks. If it's regular text node
// returning false so that it's transformed into code highlight node
return $isCodeHighlightNode(nodeA) && $isCodeHighlightNode(nodeB) && nodeA.__text === nodeB.__text && nodeA.__highlightType === nodeB.__highlightType && nodeA.__style === nodeB.__style || $isTabNode(nodeA) && $isTabNode(nodeB) || $isLineBreakNode(nodeA) && $isLineBreakNode(nodeB);
}
/**
* Returns a boolean.
* Check that the selection span is within a single CodeNode.
* This is used to guard against executing handlers that can only be
* applied in a single CodeNode context
*/
function $isSelectionInCode(selection) {
if (!$isRangeSelection(selection)) {
return false;
}
var anchorNode = selection.anchor.getNode();
var maybeAnchorCodeNode = $isCodeNode(anchorNode) ? anchorNode : anchorNode.getParent();
var focusNode = selection.focus.getNode();
var maybeFocusCodeNode = $isCodeNode(focusNode) ? focusNode : focusNode.getParent();
return $isCodeNode(maybeAnchorCodeNode) && maybeAnchorCodeNode.is(maybeFocusCodeNode);
}
/**
* Returns an Array of code lines
* Take the sequence of LineBreakNode | TabNode | CodeHighlightNode forming
* the selection and split it by LineBreakNode.
* If the selection ends at the start of the last line, it is considered empty.
* Empty lines are discarded.
*/
function $getCodeLines(selection) {
var nodes = selection.getNodes();
var lines = [];
if (nodes.length === 1 && $isCodeNode(nodes[0])) {
return lines;
}
var lastLine = [];
var _iterator = _createForOfIteratorHelper(nodes),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var node = _step.value;
invariant($isCodeHighlightNode(node) || $isTabNode(node) || $isLineBreakNode(node), 'Expected selection to be inside CodeBlock and consisting of CodeHighlightNode, TabNode and LineBreakNode');
if ($isLineBreakNode(node)) {
if (lastLine.length > 0) {
lines.push(lastLine);
lastLine = [];
}
} else {
lastLine.push(node);
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
if (lastLine.length > 0) {
var selectionEnd = selection.isBackward() ? selection.anchor : selection.focus;
// Discard the last line if the selection ends exactly at the
// start of the line (no real selection)
var lastPoint = $createPoint(lastLine[0].getKey(), 0, 'text');
if (!selectionEnd.is(lastPoint)) {
lines.push(lastLine);
}
}
return lines;
}
function $handleTab(shiftKey) {
var selection = $getSelection();
if (!$isRangeSelection(selection) || !$isSelectionInCode(selection)) {
return null;
}
var indentOrOutdent = !shiftKey ? INDENT_CONTENT_COMMAND : OUTDENT_CONTENT_COMMAND;
var tabOrOutdent = !shiftKey ? INSERT_TAB_COMMAND : OUTDENT_CONTENT_COMMAND;
var anchor = selection.anchor;
var focus = selection.focus;
// 1. early decision when there is no real selection
if (anchor.is(focus)) {
return tabOrOutdent;
}
// 2. If only empty lines or multiple non-empty lines are selected: indent/outdent
var codeLines = $getCodeLines(selection);
if (codeLines.length !== 1) {
return indentOrOutdent;
}
var codeLine = codeLines[0];
var codeLineLength = codeLine.length;
invariant(codeLineLength !== 0, '$getCodeLines only extracts non-empty lines');
// Take into account the direction of the selection
var selectionFirst;
var selectionLast;
if (selection.isBackward()) {
selectionFirst = focus;
selectionLast = anchor;
} else {
selectionFirst = anchor;
selectionLast = focus;
}
// find boundary elements of the line
// since codeLine only contains TabNode | CodeHighlightNode
// the result of these functions should is of Type TabNode | CodeHighlightNode
var firstOfLine = $getFirstCodeNodeOfLine(codeLine[0]);
var lastOfLine = $getLastCodeNodeOfLine(codeLine[0]);
var anchorOfLine = $createPoint(firstOfLine.getKey(), 0, 'text');
var focusOfLine = $createPoint(lastOfLine.getKey(), lastOfLine.getTextContentSize(), 'text');
// 3. multiline because selection started strictly before the line
if (selectionFirst.isBefore(anchorOfLine)) {
return indentOrOutdent;
}
// 4. multiline because the selection stops strictly after the line
if (focusOfLine.isBefore(selectionLast)) {
return indentOrOutdent;
}
// The selection if within the line.
// 4. If it does not touch both borders, it needs a tab
if (anchorOfLine.isBefore(selectionFirst) || selectionLast.isBefore(focusOfLine)) {
return tabOrOutdent;
}
// 5. Selection is matching a full line on non-empty code
return indentOrOutdent;
}
function $handleMultilineIndent(type) {
var selection = $getSelection();
if (!$isRangeSelection(selection) || !$isSelectionInCode(selection)) {
return false;
}
var codeLines = $getCodeLines(selection);
var codeLinesLength = codeLines.length;
// Special Indent case
// Selection is collapsed at the beginning of a line
if (codeLinesLength === 0 && selection.isCollapsed()) {
if (type === INDENT_CONTENT_COMMAND) {
selection.insertNodes([$createTabNode()]);
}
return true;
}
// Special Indent case
// Selection is matching only one LineBreak
if (codeLinesLength === 0 && type === INDENT_CONTENT_COMMAND && selection.getTextContent() === '\n') {
var tabNode = $createTabNode();
var lineBreakNode = $createLineBreakNode();
var direction = selection.isBackward() ? 'previous' : 'next';
selection.insertNodes([tabNode, lineBreakNode]);
$setSelectionFromCaretRange($getCaretRangeInDirection($getCaretRange($getTextPointCaret(tabNode, 'next', 0), $normalizeCaret($getSiblingCaret(lineBreakNode, 'next'))), direction));
return true;
}
// Indent Non Empty Lines
for (var i = 0; i < codeLinesLength; i++) {
var line = codeLines[i];
// a line here is never empty
if (line.length > 0) {
var firstOfLine = line[0];
// make sure to consider the first node on the first line
// because the line might not be fully selected
if (i === 0) {
firstOfLine = $getFirstCodeNodeOfLine(firstOfLine);
}
if (type === INDENT_CONTENT_COMMAND) {
var _tabNode = $createTabNode();
firstOfLine.insertBefore(_tabNode);
// First real code line may need selection adjustment
// when firstOfLine is at the selection boundary
if (i === 0) {
var anchorKey = selection.isBackward() ? 'focus' : 'anchor';
var anchorLine = $createPoint(firstOfLine.getKey(), 0, 'text');
if (selection[anchorKey].is(anchorLine)) {
selection[anchorKey].set(_tabNode.getKey(), 0, 'text');
}
}
} else if ($isTabNode(firstOfLine)) {
firstOfLine.remove();
}
}
}
return true;
}
function $handleShiftLines(type, event) {
// We only care about the alt+arrow keys
var selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
// I'm not quite sure why, but it seems like calling anchor.getNode() collapses the selection here
// So first, get the anchor and the focus, then get their nodes
var anchor = selection.anchor,
focus = selection.focus;
var anchorOffset = anchor.offset;
var focusOffset = focus.offset;
var anchorNode = anchor.getNode();
var focusNode = focus.getNode();
var arrowIsUp = type === KEY_ARROW_UP_COMMAND;
// Ensure the selection is within the codeblock
if (!$isSelectionInCode(selection) || !($isCodeHighlightNode(anchorNode) || $isTabNode(anchorNode)) || !($isCodeHighlightNode(focusNode) || $isTabNode(focusNode))) {
return false;
}
if (!event.altKey) {
// Handle moving selection out of the code block, given there are no
// siblings that can natively take the selection.
if (selection.isCollapsed()) {
var _codeNode = anchorNode.getParentOrThrow();
if (arrowIsUp && anchorOffset === 0 && anchorNode.getPreviousSibling() === null) {
var codeNodeSibling = _codeNode.getPreviousSibling();
if (codeNodeSibling === null) {
_codeNode.selectPrevious();
event.preventDefault();
return true;
}
} else if (!arrowIsUp && anchorOffset === anchorNode.getTextContentSize() && anchorNode.getNextSibling() === null) {
var _codeNodeSibling = _codeNode.getNextSibling();
if (_codeNodeSibling === null) {
_codeNode.selectNext();
event.preventDefault();
return true;
}
}
}
return false;
}
var start;
var end;
if (anchorNode.isBefore(focusNode)) {
start = $getFirstCodeNodeOfLine(anchorNode);
end = $getLastCodeNodeOfLine(focusNode);
} else {
start = $getFirstCodeNodeOfLine(focusNode);
end = $getLastCodeNodeOfLine(anchorNode);
}
if (start === null || end === null) {
return false;
}
var range = start.getNodesBetween(end);
var _iterator2 = _createForOfIteratorHelper(range),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var node = _step2.value;
if (!$isCodeHighlightNode(node) && !$isTabNode(node) && !$isLineBreakNode(node)) {
return false;
}
}
// After this point, we know the selection is within the codeblock. We may not be able to
// actually move the lines around, but we want to return true either way to prevent
// the event's default behavior
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
event.preventDefault();
event.stopPropagation(); // required to stop cursor movement under Firefox
var linebreak = arrowIsUp ? start.getPreviousSibling() : end.getNextSibling();
if (!$isLineBreakNode(linebreak)) {
return true;
}
var sibling = arrowIsUp ? linebreak.getPreviousSibling() : linebreak.getNextSibling();
if (sibling === null) {
return true;
}
var maybeInsertionPoint = $isCodeHighlightNode(sibling) || $isTabNode(sibling) || $isLineBreakNode(sibling) ? arrowIsUp ? $getFirstCodeNodeOfLine(sibling) : $getLastCodeNodeOfLine(sibling) : null;
var insertionPoint = maybeInsertionPoint !== null ? maybeInsertionPoint : sibling;
linebreak.remove();
range.forEach(function (node) {
return node.remove();
});
if (type === KEY_ARROW_UP_COMMAND) {
range.forEach(function (node) {
return insertionPoint.insertBefore(node);
});
insertionPoint.insertBefore(linebreak);
} else {
insertionPoint.insertAfter(linebreak);
insertionPoint = linebreak;
range.forEach(function (node) {
insertionPoint.insertAfter(node);
insertionPoint = node;
});
}
selection.setTextNodeRange(anchorNode, anchorOffset, focusNode, focusOffset);
return true;
}
function $handleMoveTo(type, event) {
var selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
var anchor = selection.anchor,
focus = selection.focus;
var anchorNode = anchor.getNode();
var focusNode = focus.getNode();
var isMoveToStart = type === MOVE_TO_START;
// Ensure the selection is within the codeblock
if (!$isSelectionInCode(selection) || !($isCodeHighlightNode(anchorNode) || $isTabNode(anchorNode)) || !($isCodeHighlightNode(focusNode) || $isTabNode(focusNode))) {
return false;
}
if (isMoveToStart) {
var start = $getStartOfCodeInLine(focusNode, focus.offset);
if (start !== null) {
var node = start.node,
offset = start.offset;
if ($isLineBreakNode(node)) {
node.selectNext(0, 0);
} else {
selection.setTextNodeRange(node, offset, node, offset);
}
} else {
focusNode.getParentOrThrow().selectStart();
}
} else {
var _node2 = $getEndOfCodeInLine(focusNode);
_node2.select();
}
event.preventDefault();
event.stopPropagation();
return true;
}
export function registerCodeHighlighting(editor, tokenizer) {
if (!editor.hasNodes([CodeNode, CodeHighlightNode])) {
throw new Error('CodeHighlightPlugin: CodeNode or CodeHighlightNode not registered on editor');
}
// eslint-disable-next-line eqeqeq
if (tokenizer == null) {
// eslint-disable-next-line no-param-reassign
tokenizer = ShikiTokenizer;
}
var registrations = [];
// Only register the mutation listener if not in headless mode
if (editor._headless !== true) {
registrations.push(editor.registerMutationListener(CodeNode, function (mutations) {
editor.update(function () {
var _iterator3 = _createForOfIteratorHelper(mutations),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var _step3$value = _slicedToArray(_step3.value, 2),
key = _step3$value[0],
type = _step3$value[1];
if (type !== 'destroyed') {
var node = $getNodeByKey(key);
if (node !== null) {
updateCodeGutter(node, editor);
}
}
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
});
}, {
skipInitialization: false
}));
}
// Add the rest of the registrations
registrations.push(editor.registerMutationListener(CodeNode, function (mutations) {
editor.update(function () {
var _iterator4 = _createForOfIteratorHelper(mutations),
_step4;
try {
var _loop = function _loop() {
var _step4$value = _slicedToArray(_step4.value, 2),
key = _step4$value[0],
type = _step4$value[1];
if (type !== 'destroyed') {
var node = $getNodeByKey(key);
if (node !== null) {
var _codeNode2$getTheme;
var _codeNode2 = node;
if ((_codeNode2$getTheme = _codeNode2.getTheme()) !== null && _codeNode2$getTheme !== void 0 && _codeNode2$getTheme.endsWith(' needUpdate')) {
editor.update(function () {
var _codeNode2$getTheme2;
_codeNode2.setTheme(((_codeNode2$getTheme2 = _codeNode2.getTheme()) === null || _codeNode2$getTheme2 === void 0 ? void 0 : _codeNode2$getTheme2.replace(' needUpdate', '')) || '');
});
}
}
}
};
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
_loop();
}
} catch (err) {
_iterator4.e(err);
} finally {
_iterator4.f();
}
});
}, {
skipInitialization: false
}), editor.registerNodeTransform(CodeNode, function (node) {
return codeNodeTransform(node, editor, tokenizer);
}), editor.registerNodeTransform(TextNode, function (node) {
return $textNodeTransform(node, editor, tokenizer);
}), editor.registerNodeTransform(CodeHighlightNode, function (node) {
return $textNodeTransform(node, editor, tokenizer);
}), editor.registerCommand(KEY_TAB_COMMAND, function (event) {
var command = $handleTab(event.shiftKey);
if (command === null) {
return false;
}
event.preventDefault();
editor.dispatchCommand(command, undefined);
return true;
}, COMMAND_PRIORITY_LOW), editor.registerCommand(INSERT_TAB_COMMAND, function () {
var selection = $getSelection();
if (!$isSelectionInCode(selection)) {
return false;
}
$insertNodes([$createTabNode()]);
return true;
}, COMMAND_PRIORITY_LOW), editor.registerCommand(INDENT_CONTENT_COMMAND, function () {
return $handleMultilineIndent(INDENT_CONTENT_COMMAND);
}, COMMAND_PRIORITY_LOW), editor.registerCommand(OUTDENT_CONTENT_COMMAND, function () {
return $handleMultilineIndent(OUTDENT_CONTENT_COMMAND);
}, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ARROW_UP_COMMAND, function (event) {
var selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
var anchor = selection.anchor;
var anchorNode = anchor.getNode();
if (!$isSelectionInCode(selection)) {
return false;
}
// If at the start of a code block, prevent selection from moving out
if (selection.isCollapsed() && anchor.offset === 0 && anchorNode.getPreviousSibling() === null && $isCodeNode(anchorNode.getParentOrThrow())) {
event.preventDefault();
return true;
}
return $handleShiftLines(KEY_ARROW_UP_COMMAND, event);
}, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ARROW_DOWN_COMMAND, function (event) {
var selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
var anchor = selection.anchor;
var anchorNode = anchor.getNode();
if (!$isSelectionInCode(selection)) {
return false;
}
// If at the end of a code block, prevent selection from moving out
if (selection.isCollapsed() && anchor.offset === anchorNode.getTextContentSize() && anchorNode.getNextSibling() === null && $isCodeNode(anchorNode.getParentOrThrow())) {
event.preventDefault();
return true;
}
return $handleShiftLines(KEY_ARROW_DOWN_COMMAND, event);
}, COMMAND_PRIORITY_LOW), editor.registerCommand(MOVE_TO_START, function (event) {
return $handleMoveTo(MOVE_TO_START, event);
}, COMMAND_PRIORITY_LOW), editor.registerCommand(MOVE_TO_END, function (event) {
return $handleMoveTo(MOVE_TO_END, event);
}, COMMAND_PRIORITY_LOW));
return mergeRegister.apply(void 0, registrations);
}