Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
1555 lines
52 KiB
Plaintext
1555 lines
52 KiB
Plaintext
/**
|
|
* 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.
|
|
*
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var utils = require('@lexical/utils');
|
|
var lexical = require('lexical');
|
|
require('prismjs');
|
|
require('prismjs/components/prism-clike');
|
|
require('prismjs/components/prism-javascript');
|
|
require('prismjs/components/prism-markup');
|
|
require('prismjs/components/prism-markdown');
|
|
require('prismjs/components/prism-c');
|
|
require('prismjs/components/prism-css');
|
|
require('prismjs/components/prism-objectivec');
|
|
require('prismjs/components/prism-sql');
|
|
require('prismjs/components/prism-powershell');
|
|
require('prismjs/components/prism-python');
|
|
require('prismjs/components/prism-rust');
|
|
require('prismjs/components/prism-swift');
|
|
require('prismjs/components/prism-typescript');
|
|
require('prismjs/components/prism-java');
|
|
require('prismjs/components/prism-cpp');
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
*/
|
|
|
|
// Do not require this module directly! Use normal `invariant` calls.
|
|
|
|
function formatDevErrorMessage(message) {
|
|
throw new Error(message);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
*/
|
|
|
|
// invariant(condition, message) will refine types based on "condition", and
|
|
// if "condition" is false will throw an error. This function is special-cased
|
|
// in flow itself, so we can't name it anything else.
|
|
function invariant(cond, message, ...args) {
|
|
if (cond) {
|
|
return;
|
|
}
|
|
throw new Error('Internal Lexical error: invariant() is meant to be replaced at compile ' + 'time. There is no runtime version. Error: ' + message);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
*/
|
|
|
|
const DEFAULT_CODE_LANGUAGE = 'javascript';
|
|
const getDefaultCodeLanguage = () => DEFAULT_CODE_LANGUAGE;
|
|
function hasChildDOMNodeTag(node, tagName) {
|
|
for (const child of node.childNodes) {
|
|
if (utils.isHTMLElement(child) && child.tagName === tagName) {
|
|
return true;
|
|
}
|
|
hasChildDOMNodeTag(child, tagName);
|
|
}
|
|
return false;
|
|
}
|
|
const LANGUAGE_DATA_ATTRIBUTE = 'data-language';
|
|
const HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE = 'data-highlight-language';
|
|
const THEME_DATA_ATTRIBUTE = 'data-theme';
|
|
|
|
/** @noInheritDoc */
|
|
class CodeNode extends lexical.ElementNode {
|
|
/** @internal */
|
|
|
|
/** @internal */
|
|
|
|
/** @internal */
|
|
|
|
static getType() {
|
|
return 'code';
|
|
}
|
|
static clone(node) {
|
|
return new CodeNode(node.__language, node.__key);
|
|
}
|
|
constructor(language, key) {
|
|
super(key);
|
|
this.__language = language || undefined;
|
|
this.__isSyntaxHighlightSupported = false;
|
|
this.__theme = undefined;
|
|
}
|
|
afterCloneFrom(prevNode) {
|
|
super.afterCloneFrom(prevNode);
|
|
this.__language = prevNode.__language;
|
|
this.__theme = prevNode.__theme;
|
|
this.__isSyntaxHighlightSupported = prevNode.__isSyntaxHighlightSupported;
|
|
}
|
|
|
|
// View
|
|
createDOM(config) {
|
|
const element = document.createElement('code');
|
|
utils.addClassNamesToElement(element, config.theme.code);
|
|
element.setAttribute('spellcheck', 'false');
|
|
const language = this.getLanguage();
|
|
if (language) {
|
|
element.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
|
|
if (this.getIsSyntaxHighlightSupported()) {
|
|
element.setAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE, language);
|
|
}
|
|
}
|
|
const theme = this.getTheme();
|
|
if (theme) {
|
|
element.setAttribute(THEME_DATA_ATTRIBUTE, theme);
|
|
}
|
|
const style = this.getStyle();
|
|
if (style) {
|
|
element.setAttribute('style', style);
|
|
}
|
|
return element;
|
|
}
|
|
updateDOM(prevNode, dom, config) {
|
|
const language = this.__language;
|
|
const prevLanguage = prevNode.__language;
|
|
if (language) {
|
|
if (language !== prevLanguage) {
|
|
dom.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
|
|
}
|
|
} else if (prevLanguage) {
|
|
dom.removeAttribute(LANGUAGE_DATA_ATTRIBUTE);
|
|
}
|
|
const isSyntaxHighlightSupported = this.__isSyntaxHighlightSupported;
|
|
const prevIsSyntaxHighlightSupported = prevNode.__isSyntaxHighlightSupported;
|
|
if (prevIsSyntaxHighlightSupported && prevLanguage) {
|
|
if (isSyntaxHighlightSupported && language) {
|
|
if (language !== prevLanguage) {
|
|
dom.setAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE, language);
|
|
}
|
|
} else {
|
|
dom.removeAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE);
|
|
}
|
|
} else if (isSyntaxHighlightSupported && language) {
|
|
dom.setAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE, language);
|
|
}
|
|
const theme = this.__theme;
|
|
const prevTheme = prevNode.__theme;
|
|
if (theme) {
|
|
if (theme !== prevTheme) {
|
|
dom.setAttribute(THEME_DATA_ATTRIBUTE, theme);
|
|
}
|
|
} else if (prevTheme) {
|
|
dom.removeAttribute(THEME_DATA_ATTRIBUTE);
|
|
}
|
|
const style = this.__style;
|
|
const prevStyle = prevNode.__style;
|
|
if (style) {
|
|
if (style !== prevStyle) {
|
|
dom.setAttribute('style', style);
|
|
}
|
|
} else if (prevStyle) {
|
|
dom.removeAttribute('style');
|
|
}
|
|
return false;
|
|
}
|
|
exportDOM(editor) {
|
|
const element = document.createElement('pre');
|
|
utils.addClassNamesToElement(element, editor._config.theme.code);
|
|
element.setAttribute('spellcheck', 'false');
|
|
const language = this.getLanguage();
|
|
if (language) {
|
|
element.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
|
|
if (this.getIsSyntaxHighlightSupported()) {
|
|
element.setAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE, language);
|
|
}
|
|
}
|
|
const theme = this.getTheme();
|
|
if (theme) {
|
|
element.setAttribute(THEME_DATA_ATTRIBUTE, theme);
|
|
}
|
|
const style = this.getStyle();
|
|
if (style) {
|
|
element.setAttribute('style', style);
|
|
}
|
|
return {
|
|
element
|
|
};
|
|
}
|
|
static importDOM() {
|
|
return {
|
|
// Typically <pre> is used for code blocks, and <code> for inline code styles
|
|
// but if it's a multi line <code> we'll create a block. Pass through to
|
|
// inline format handled by TextNode otherwise.
|
|
code: node => {
|
|
const isMultiLine = node.textContent != null && (/\r?\n/.test(node.textContent) || hasChildDOMNodeTag(node, 'BR'));
|
|
return isMultiLine ? {
|
|
conversion: $convertPreElement,
|
|
priority: 1
|
|
} : null;
|
|
},
|
|
div: () => ({
|
|
conversion: $convertDivElement,
|
|
priority: 1
|
|
}),
|
|
pre: () => ({
|
|
conversion: $convertPreElement,
|
|
priority: 0
|
|
}),
|
|
table: node => {
|
|
const table = node;
|
|
// domNode is a <table> since we matched it by nodeName
|
|
if (isGitHubCodeTable(table)) {
|
|
return {
|
|
conversion: $convertTableElement,
|
|
priority: 3
|
|
};
|
|
}
|
|
return null;
|
|
},
|
|
td: node => {
|
|
// element is a <td> since we matched it by nodeName
|
|
const td = node;
|
|
const table = td.closest('table');
|
|
if (isGitHubCodeCell(td) || table && isGitHubCodeTable(table)) {
|
|
// Return a no-op if it's a table cell in a code table, but not a code line.
|
|
// Otherwise it'll fall back to the T
|
|
return {
|
|
conversion: convertCodeNoop,
|
|
priority: 3
|
|
};
|
|
}
|
|
return null;
|
|
},
|
|
tr: node => {
|
|
// element is a <tr> since we matched it by nodeName
|
|
const tr = node;
|
|
const table = tr.closest('table');
|
|
if (table && isGitHubCodeTable(table)) {
|
|
return {
|
|
conversion: convertCodeNoop,
|
|
priority: 3
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
}
|
|
static importJSON(serializedNode) {
|
|
return $createCodeNode().updateFromJSON(serializedNode);
|
|
}
|
|
updateFromJSON(serializedNode) {
|
|
return super.updateFromJSON(serializedNode).setLanguage(serializedNode.language).setTheme(serializedNode.theme);
|
|
}
|
|
exportJSON() {
|
|
return {
|
|
...super.exportJSON(),
|
|
language: this.getLanguage(),
|
|
theme: this.getTheme()
|
|
};
|
|
}
|
|
|
|
// Mutation
|
|
insertNewAfter(selection, restoreSelection = true) {
|
|
const children = this.getChildren();
|
|
const childrenLength = children.length;
|
|
if (childrenLength >= 2 && children[childrenLength - 1].getTextContent() === '\n' && children[childrenLength - 2].getTextContent() === '\n' && selection.isCollapsed() && selection.anchor.key === this.__key && selection.anchor.offset === childrenLength) {
|
|
children[childrenLength - 1].remove();
|
|
children[childrenLength - 2].remove();
|
|
const newElement = lexical.$createParagraphNode();
|
|
this.insertAfter(newElement, restoreSelection);
|
|
return newElement;
|
|
}
|
|
|
|
// If the selection is within the codeblock, find all leading tabs and
|
|
// spaces of the current line. Create a new line that has all those
|
|
// tabs and spaces, such that leading indentation is preserved.
|
|
const {
|
|
anchor,
|
|
focus
|
|
} = selection;
|
|
const firstPoint = anchor.isBefore(focus) ? anchor : focus;
|
|
const firstSelectionNode = firstPoint.getNode();
|
|
if (lexical.$isTextNode(firstSelectionNode)) {
|
|
let node = $getFirstCodeNodeOfLine(firstSelectionNode);
|
|
const insertNodes = [];
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (true) {
|
|
if (lexical.$isTabNode(node)) {
|
|
insertNodes.push(lexical.$createTabNode());
|
|
node = node.getNextSibling();
|
|
} else if ($isCodeHighlightNode(node)) {
|
|
let spaces = 0;
|
|
const text = node.getTextContent();
|
|
const textSize = node.getTextContentSize();
|
|
while (spaces < textSize && text[spaces] === ' ') {
|
|
spaces++;
|
|
}
|
|
if (spaces !== 0) {
|
|
insertNodes.push($createCodeHighlightNode(' '.repeat(spaces)));
|
|
}
|
|
if (spaces !== textSize) {
|
|
break;
|
|
}
|
|
node = node.getNextSibling();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
const split = firstSelectionNode.splitText(anchor.offset)[0];
|
|
const x = anchor.offset === 0 ? 0 : 1;
|
|
const index = split.getIndexWithinParent() + x;
|
|
const codeNode = firstSelectionNode.getParentOrThrow();
|
|
const nodesToInsert = [lexical.$createLineBreakNode(), ...insertNodes];
|
|
codeNode.splice(index, 0, nodesToInsert);
|
|
const last = insertNodes[insertNodes.length - 1];
|
|
if (last) {
|
|
last.select();
|
|
} else if (anchor.offset === 0) {
|
|
split.selectPrevious();
|
|
} else {
|
|
split.getNextSibling().selectNext(0, 0);
|
|
}
|
|
}
|
|
if ($isCodeNode(firstSelectionNode)) {
|
|
const {
|
|
offset
|
|
} = selection.anchor;
|
|
firstSelectionNode.splice(offset, 0, [lexical.$createLineBreakNode()]);
|
|
firstSelectionNode.select(offset + 1, offset + 1);
|
|
}
|
|
return null;
|
|
}
|
|
canIndent() {
|
|
return false;
|
|
}
|
|
collapseAtStart() {
|
|
const paragraph = lexical.$createParagraphNode();
|
|
const children = this.getChildren();
|
|
children.forEach(child => paragraph.append(child));
|
|
this.replace(paragraph);
|
|
return true;
|
|
}
|
|
setLanguage(language) {
|
|
const writable = this.getWritable();
|
|
writable.__language = language || undefined;
|
|
return writable;
|
|
}
|
|
getLanguage() {
|
|
return this.getLatest().__language;
|
|
}
|
|
setIsSyntaxHighlightSupported(isSupported) {
|
|
const writable = this.getWritable();
|
|
writable.__isSyntaxHighlightSupported = isSupported;
|
|
return writable;
|
|
}
|
|
getIsSyntaxHighlightSupported() {
|
|
return this.getLatest().__isSyntaxHighlightSupported;
|
|
}
|
|
setTheme(theme) {
|
|
const writable = this.getWritable();
|
|
writable.__theme = theme || undefined;
|
|
return writable;
|
|
}
|
|
getTheme() {
|
|
return this.getLatest().__theme;
|
|
}
|
|
}
|
|
function $createCodeNode(language, theme) {
|
|
return lexical.$create(CodeNode).setLanguage(language).setTheme(theme);
|
|
}
|
|
function $isCodeNode(node) {
|
|
return node instanceof CodeNode;
|
|
}
|
|
function $convertPreElement(domNode) {
|
|
const language = domNode.getAttribute(LANGUAGE_DATA_ATTRIBUTE);
|
|
return {
|
|
node: $createCodeNode(language)
|
|
};
|
|
}
|
|
function $convertDivElement(domNode) {
|
|
// domNode is a <div> since we matched it by nodeName
|
|
const div = domNode;
|
|
const isCode = isCodeElement(div);
|
|
if (!isCode && !isCodeChildElement(div)) {
|
|
return {
|
|
node: null
|
|
};
|
|
}
|
|
return {
|
|
node: isCode ? $createCodeNode() : null
|
|
};
|
|
}
|
|
function $convertTableElement() {
|
|
return {
|
|
node: $createCodeNode()
|
|
};
|
|
}
|
|
function convertCodeNoop() {
|
|
return {
|
|
node: null
|
|
};
|
|
}
|
|
function isCodeElement(div) {
|
|
return div.style.fontFamily.match('monospace') !== null;
|
|
}
|
|
function isCodeChildElement(node) {
|
|
let parent = node.parentElement;
|
|
while (parent !== null) {
|
|
if (isCodeElement(parent)) {
|
|
return true;
|
|
}
|
|
parent = parent.parentElement;
|
|
}
|
|
return false;
|
|
}
|
|
function isGitHubCodeCell(cell) {
|
|
return cell.classList.contains('js-file-line');
|
|
}
|
|
function isGitHubCodeTable(table) {
|
|
return table.classList.contains('js-file-line-container');
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
*/
|
|
|
|
/** @noInheritDoc */
|
|
class CodeHighlightNode extends lexical.TextNode {
|
|
/** @internal */
|
|
|
|
constructor(text = '', highlightType, key) {
|
|
super(text, key);
|
|
this.__highlightType = highlightType;
|
|
}
|
|
static getType() {
|
|
return 'code-highlight';
|
|
}
|
|
static clone(node) {
|
|
return new CodeHighlightNode(node.__text, node.__highlightType || undefined, node.__key);
|
|
}
|
|
getHighlightType() {
|
|
const self = this.getLatest();
|
|
return self.__highlightType;
|
|
}
|
|
setHighlightType(highlightType) {
|
|
const self = this.getWritable();
|
|
self.__highlightType = highlightType || undefined;
|
|
return self;
|
|
}
|
|
canHaveFormat() {
|
|
return false;
|
|
}
|
|
createDOM(config) {
|
|
const element = super.createDOM(config);
|
|
const className = getHighlightThemeClass(config.theme, this.__highlightType);
|
|
utils.addClassNamesToElement(element, className);
|
|
return element;
|
|
}
|
|
updateDOM(prevNode, dom, config) {
|
|
const update = super.updateDOM(prevNode, dom, config);
|
|
const prevClassName = getHighlightThemeClass(config.theme, prevNode.__highlightType);
|
|
const nextClassName = getHighlightThemeClass(config.theme, this.__highlightType);
|
|
if (prevClassName !== nextClassName) {
|
|
if (prevClassName) {
|
|
utils.removeClassNamesFromElement(dom, prevClassName);
|
|
}
|
|
if (nextClassName) {
|
|
utils.addClassNamesToElement(dom, nextClassName);
|
|
}
|
|
}
|
|
return update;
|
|
}
|
|
static importJSON(serializedNode) {
|
|
return $createCodeHighlightNode().updateFromJSON(serializedNode);
|
|
}
|
|
updateFromJSON(serializedNode) {
|
|
return super.updateFromJSON(serializedNode).setHighlightType(serializedNode.highlightType);
|
|
}
|
|
exportJSON() {
|
|
return {
|
|
...super.exportJSON(),
|
|
highlightType: this.getHighlightType()
|
|
};
|
|
}
|
|
|
|
// Prevent formatting (bold, underline, etc)
|
|
setFormat(format) {
|
|
return this;
|
|
}
|
|
isParentRequired() {
|
|
return true;
|
|
}
|
|
createParentElementNode() {
|
|
return $createCodeNode();
|
|
}
|
|
}
|
|
function getHighlightThemeClass(theme, highlightType) {
|
|
return highlightType && theme && theme.codeHighlight && theme.codeHighlight[highlightType];
|
|
}
|
|
function $createCodeHighlightNode(text = '', highlightType) {
|
|
return lexical.$applyNodeReplacement(new CodeHighlightNode(text, highlightType));
|
|
}
|
|
function $isCodeHighlightNode(node) {
|
|
return node instanceof CodeHighlightNode;
|
|
}
|
|
|
|
function $getLastMatchingCodeNode(anchor, direction) {
|
|
let matchingNode = anchor;
|
|
for (let caret = lexical.$getSiblingCaret(anchor, direction); caret && ($isCodeHighlightNode(caret.origin) || lexical.$isTabNode(caret.origin)); caret = utils.$getAdjacentCaret(caret)) {
|
|
matchingNode = caret.origin;
|
|
}
|
|
return matchingNode;
|
|
}
|
|
function $getFirstCodeNodeOfLine(anchor) {
|
|
return $getLastMatchingCodeNode(anchor, 'previous');
|
|
}
|
|
function $getLastCodeNodeOfLine(anchor) {
|
|
return $getLastMatchingCodeNode(anchor, 'next');
|
|
}
|
|
function $getStartOfCodeInLine(anchor, offset) {
|
|
let last = null;
|
|
let lastNonBlank = null;
|
|
let node = anchor;
|
|
let nodeOffset = offset;
|
|
let nodeTextContent = anchor.getTextContent();
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (true) {
|
|
if (nodeOffset === 0) {
|
|
node = node.getPreviousSibling();
|
|
if (node === null) {
|
|
break;
|
|
}
|
|
if (!($isCodeHighlightNode(node) || lexical.$isTabNode(node) || lexical.$isLineBreakNode(node))) {
|
|
formatDevErrorMessage(`Expected a valid Code Node: CodeHighlightNode, TabNode, LineBreakNode`);
|
|
}
|
|
if (lexical.$isLineBreakNode(node)) {
|
|
last = {
|
|
node,
|
|
offset: 1
|
|
};
|
|
break;
|
|
}
|
|
nodeOffset = Math.max(0, node.getTextContentSize() - 1);
|
|
nodeTextContent = node.getTextContent();
|
|
} else {
|
|
nodeOffset--;
|
|
}
|
|
const character = nodeTextContent[nodeOffset];
|
|
if ($isCodeHighlightNode(node) && character !== ' ') {
|
|
lastNonBlank = {
|
|
node,
|
|
offset: nodeOffset
|
|
};
|
|
}
|
|
}
|
|
// lastNonBlank !== null: anchor in the middle of code; move to line beginning
|
|
if (lastNonBlank !== null) {
|
|
return lastNonBlank;
|
|
}
|
|
// Spaces, tabs or nothing ahead of anchor
|
|
let codeCharacterAtAnchorOffset = null;
|
|
if (offset < anchor.getTextContentSize()) {
|
|
if ($isCodeHighlightNode(anchor)) {
|
|
codeCharacterAtAnchorOffset = anchor.getTextContent()[offset];
|
|
}
|
|
} else {
|
|
const nextSibling = anchor.getNextSibling();
|
|
if ($isCodeHighlightNode(nextSibling)) {
|
|
codeCharacterAtAnchorOffset = nextSibling.getTextContent()[0];
|
|
}
|
|
}
|
|
if (codeCharacterAtAnchorOffset !== null && codeCharacterAtAnchorOffset !== ' ') {
|
|
// Borderline whitespace and code, move to line beginning
|
|
return last;
|
|
} else {
|
|
const nextNonBlank = findNextNonBlankInLine(anchor, offset);
|
|
if (nextNonBlank !== null) {
|
|
return nextNonBlank;
|
|
} else {
|
|
return last;
|
|
}
|
|
}
|
|
}
|
|
function findNextNonBlankInLine(anchor, offset) {
|
|
let node = anchor;
|
|
let nodeOffset = offset;
|
|
let nodeTextContent = anchor.getTextContent();
|
|
let nodeTextContentSize = anchor.getTextContentSize();
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (true) {
|
|
if (!$isCodeHighlightNode(node) || nodeOffset === nodeTextContentSize) {
|
|
node = node.getNextSibling();
|
|
if (node === null || lexical.$isLineBreakNode(node)) {
|
|
return null;
|
|
}
|
|
if ($isCodeHighlightNode(node)) {
|
|
nodeOffset = 0;
|
|
nodeTextContent = node.getTextContent();
|
|
nodeTextContentSize = node.getTextContentSize();
|
|
}
|
|
}
|
|
if ($isCodeHighlightNode(node)) {
|
|
if (nodeTextContent[nodeOffset] !== ' ') {
|
|
return {
|
|
node,
|
|
offset: nodeOffset
|
|
};
|
|
}
|
|
nodeOffset++;
|
|
}
|
|
}
|
|
}
|
|
function $getEndOfCodeInLine(anchor) {
|
|
const lastNode = $getLastCodeNodeOfLine(anchor);
|
|
if (!!lexical.$isLineBreakNode(lastNode)) {
|
|
formatDevErrorMessage(`Unexpected lineBreakNode in getEndOfCodeInLine`);
|
|
}
|
|
return lastNode;
|
|
}
|
|
|
|
(function (Prism) {
|
|
|
|
Prism.languages.diff = {
|
|
'coord': [
|
|
// Match all kinds of coord lines (prefixed by "+++", "---" or "***").
|
|
/^(?:\*{3}|-{3}|\+{3}).*$/m,
|
|
// Match "@@ ... @@" coord lines in unified diff.
|
|
/^@@.*@@$/m,
|
|
// Match coord lines in normal diff (starts with a number).
|
|
/^\d.*$/m
|
|
]
|
|
|
|
// deleted, inserted, unchanged, diff
|
|
};
|
|
|
|
/**
|
|
* A map from the name of a block to its line prefix.
|
|
*
|
|
* @type {Object<string, string>}
|
|
*/
|
|
var PREFIXES = {
|
|
'deleted-sign': '-',
|
|
'deleted-arrow': '<',
|
|
'inserted-sign': '+',
|
|
'inserted-arrow': '>',
|
|
'unchanged': ' ',
|
|
'diff': '!',
|
|
};
|
|
|
|
// add a token for each prefix
|
|
Object.keys(PREFIXES).forEach(function (name) {
|
|
var prefix = PREFIXES[name];
|
|
|
|
var alias = [];
|
|
if (!/^\w+$/.test(name)) { // "deleted-sign" -> "deleted"
|
|
alias.push(/\w+/.exec(name)[0]);
|
|
}
|
|
if (name === 'diff') {
|
|
alias.push('bold');
|
|
}
|
|
|
|
Prism.languages.diff[name] = {
|
|
pattern: RegExp('^(?:[' + prefix + '].*(?:\r\n?|\n|(?![\\s\\S])))+', 'm'),
|
|
alias: alias,
|
|
inside: {
|
|
'line': {
|
|
pattern: /(.)(?=[\s\S]).*(?:\r\n?|\n)?/,
|
|
lookbehind: true
|
|
},
|
|
'prefix': {
|
|
pattern: /[\s\S]/,
|
|
alias: /\w+/.exec(name)[0]
|
|
}
|
|
}
|
|
};
|
|
|
|
});
|
|
|
|
// make prefixes available to Diff plugin
|
|
Object.defineProperty(Prism.languages.diff, 'PREFIXES', {
|
|
value: PREFIXES
|
|
});
|
|
|
|
}(Prism));
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
*/
|
|
|
|
const Prism$1 = globalThis.Prism || window.Prism;
|
|
const CODE_LANGUAGE_FRIENDLY_NAME_MAP = {
|
|
c: 'C',
|
|
clike: 'C-like',
|
|
cpp: 'C++',
|
|
css: 'CSS',
|
|
html: 'HTML',
|
|
java: 'Java',
|
|
js: 'JavaScript',
|
|
markdown: 'Markdown',
|
|
objc: 'Objective-C',
|
|
plain: 'Plain Text',
|
|
powershell: 'PowerShell',
|
|
py: 'Python',
|
|
rust: 'Rust',
|
|
sql: 'SQL',
|
|
swift: 'Swift',
|
|
typescript: 'TypeScript',
|
|
xml: 'XML'
|
|
};
|
|
const CODE_LANGUAGE_MAP = {
|
|
cpp: 'cpp',
|
|
java: 'java',
|
|
javascript: 'js',
|
|
md: 'markdown',
|
|
plaintext: 'plain',
|
|
python: 'py',
|
|
text: 'plain',
|
|
ts: 'typescript'
|
|
};
|
|
function normalizeCodeLang(lang) {
|
|
return CODE_LANGUAGE_MAP[lang] || lang;
|
|
}
|
|
function getLanguageFriendlyName(lang) {
|
|
const _lang = normalizeCodeLang(lang);
|
|
return CODE_LANGUAGE_FRIENDLY_NAME_MAP[_lang] || _lang;
|
|
}
|
|
const getCodeLanguages = () => Object.keys(Prism$1.languages).filter(
|
|
// Prism has several language helpers mixed into languages object
|
|
// so filtering them out here to get langs list
|
|
language => typeof Prism$1.languages[language] !== 'function').sort();
|
|
function getCodeLanguageOptions() {
|
|
const options = [];
|
|
for (const [lang, friendlyName] of Object.entries(CODE_LANGUAGE_FRIENDLY_NAME_MAP)) {
|
|
options.push([lang, friendlyName]);
|
|
}
|
|
return options;
|
|
}
|
|
|
|
// Prism has no theme support
|
|
function getCodeThemeOptions() {
|
|
const options = [];
|
|
return options;
|
|
}
|
|
function getDiffedLanguage(language) {
|
|
const DIFF_LANGUAGE_REGEX = /^diff-([\w-]+)/i;
|
|
const diffLanguageMatch = DIFF_LANGUAGE_REGEX.exec(language);
|
|
return diffLanguageMatch ? diffLanguageMatch[1] : null;
|
|
}
|
|
function isCodeLanguageLoaded(language) {
|
|
const diffedLanguage = getDiffedLanguage(language);
|
|
const langId = diffedLanguage ? diffedLanguage : language;
|
|
try {
|
|
// eslint-disable-next-line no-prototype-builtins
|
|
return langId ? Prism$1.languages.hasOwnProperty(langId) : false;
|
|
} catch (_unused) {
|
|
return false;
|
|
}
|
|
}
|
|
async function loadCodeLanguage(language, editor, codeNodeKey) {
|
|
// NOT IMPLEMENTED
|
|
}
|
|
function getTextContent(token) {
|
|
if (typeof token === 'string') {
|
|
return token;
|
|
} else if (Array.isArray(token)) {
|
|
return token.map(getTextContent).join('');
|
|
} else {
|
|
return getTextContent(token.content);
|
|
}
|
|
}
|
|
|
|
// The following code is extracted/adapted from prismjs v2
|
|
// It will probably be possible to use it directly from prism v2
|
|
// in the future when prismjs v2 is published and Lexical upgrades
|
|
// the prismsjs dependency
|
|
function tokenizeDiffHighlight(tokens, language) {
|
|
const diffLanguage = language;
|
|
const diffGrammar = Prism$1.languages[diffLanguage];
|
|
const env = {
|
|
tokens
|
|
};
|
|
const PREFIXES = Prism$1.languages.diff.PREFIXES;
|
|
for (const token of env.tokens) {
|
|
if (typeof token === 'string' || !(token.type in PREFIXES) || !Array.isArray(token.content)) {
|
|
continue;
|
|
}
|
|
const type = token.type;
|
|
let insertedPrefixes = 0;
|
|
const getPrefixToken = () => {
|
|
insertedPrefixes++;
|
|
return new Prism$1.Token('prefix', PREFIXES[type], type.replace(/^(\w+).*/, '$1'));
|
|
};
|
|
const withoutPrefixes = token.content.filter(t => typeof t === 'string' || t.type !== 'prefix');
|
|
const prefixCount = token.content.length - withoutPrefixes.length;
|
|
const diffTokens = Prism$1.tokenize(getTextContent(withoutPrefixes), diffGrammar);
|
|
|
|
// re-insert prefixes
|
|
// always add a prefix at the start
|
|
diffTokens.unshift(getPrefixToken());
|
|
const LINE_BREAK = /\r\n|\n/g;
|
|
const insertAfterLineBreakString = text => {
|
|
const result = [];
|
|
LINE_BREAK.lastIndex = 0;
|
|
let last = 0;
|
|
let m;
|
|
while (insertedPrefixes < prefixCount && (m = LINE_BREAK.exec(text))) {
|
|
const end = m.index + m[0].length;
|
|
result.push(text.slice(last, end));
|
|
last = end;
|
|
result.push(getPrefixToken());
|
|
}
|
|
if (result.length === 0) {
|
|
return undefined;
|
|
}
|
|
if (last < text.length) {
|
|
result.push(text.slice(last));
|
|
}
|
|
return result;
|
|
};
|
|
const insertAfterLineBreak = toks => {
|
|
for (let i = 0; i < toks.length && insertedPrefixes < prefixCount; i++) {
|
|
const tok = toks[i];
|
|
if (typeof tok === 'string') {
|
|
const inserted = insertAfterLineBreakString(tok);
|
|
if (inserted) {
|
|
toks.splice(i, 1, ...inserted);
|
|
i += inserted.length - 1;
|
|
}
|
|
} else if (typeof tok.content === 'string') {
|
|
const inserted = insertAfterLineBreakString(tok.content);
|
|
if (inserted) {
|
|
tok.content = inserted;
|
|
}
|
|
} else if (Array.isArray(tok.content)) {
|
|
insertAfterLineBreak(tok.content);
|
|
} else {
|
|
insertAfterLineBreak([tok.content]);
|
|
}
|
|
}
|
|
};
|
|
insertAfterLineBreak(diffTokens);
|
|
if (insertedPrefixes < prefixCount) {
|
|
// we are missing the last prefix
|
|
diffTokens.push(getPrefixToken());
|
|
}
|
|
token.content = diffTokens;
|
|
}
|
|
return env.tokens;
|
|
}
|
|
function $getHighlightNodes(codeNode, language) {
|
|
const DIFF_LANGUAGE_REGEX = /^diff-([\w-]+)/i;
|
|
const diffLanguageMatch = DIFF_LANGUAGE_REGEX.exec(language);
|
|
const code = codeNode.getTextContent();
|
|
let tokens = Prism$1.tokenize(code, Prism$1.languages[diffLanguageMatch ? 'diff' : language]);
|
|
if (diffLanguageMatch) {
|
|
tokens = tokenizeDiffHighlight(tokens, diffLanguageMatch[1]);
|
|
}
|
|
return $mapTokensToLexicalStructure(tokens);
|
|
}
|
|
function $mapTokensToLexicalStructure(tokens, type) {
|
|
const nodes = [];
|
|
for (const token of tokens) {
|
|
if (typeof token === 'string') {
|
|
const partials = token.split(/(\n|\t)/);
|
|
const partialsLength = partials.length;
|
|
for (let i = 0; i < partialsLength; i++) {
|
|
const part = partials[i];
|
|
if (part === '\n' || part === '\r\n') {
|
|
nodes.push(lexical.$createLineBreakNode());
|
|
} else if (part === '\t') {
|
|
nodes.push(lexical.$createTabNode());
|
|
} else if (part.length > 0) {
|
|
nodes.push($createCodeHighlightNode(part, type));
|
|
}
|
|
}
|
|
} else {
|
|
const {
|
|
content,
|
|
alias
|
|
} = token;
|
|
if (typeof content === 'string') {
|
|
nodes.push(...$mapTokensToLexicalStructure([content], token.type === 'prefix' && typeof alias === 'string' ? alias : token.type));
|
|
} else if (Array.isArray(content)) {
|
|
nodes.push(...$mapTokensToLexicalStructure(content, token.type === 'unchanged' ? undefined : token.type));
|
|
}
|
|
}
|
|
}
|
|
return nodes;
|
|
}
|
|
|
|
const PrismTokenizer = {
|
|
$tokenize(codeNode, language) {
|
|
return $getHighlightNodes(codeNode, language || this.defaultLanguage);
|
|
},
|
|
defaultLanguage: DEFAULT_CODE_LANGUAGE,
|
|
tokenize(code, language) {
|
|
return Prism$1.tokenize(code, Prism$1.languages[language || ''] || Prism$1.languages[this.defaultLanguage]);
|
|
}
|
|
};
|
|
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
|
|
const parentNode = node.getParent();
|
|
if ($isCodeNode(parentNode)) {
|
|
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(lexical.$createTextNode(node.__text));
|
|
}
|
|
}
|
|
function updateCodeGutter(node, editor) {
|
|
const codeElement = editor.getElementByKey(node.getKey());
|
|
if (codeElement === null) {
|
|
return;
|
|
}
|
|
const children = node.getChildren();
|
|
const 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;
|
|
let gutter = '1';
|
|
let count = 1;
|
|
for (let i = 0; i < childrenLength; i++) {
|
|
if (lexical.$isLineBreakNode(children[i])) {
|
|
gutter += '\n' + ++count;
|
|
}
|
|
}
|
|
codeElement.setAttribute('data-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.
|
|
|
|
const nodesCurrentlyHighlighting = new Set();
|
|
function codeNodeTransform(node, editor, tokenizer) {
|
|
const nodeKey = node.getKey();
|
|
|
|
// When new code block inserted it might not have language selected
|
|
if (node.getLanguage() === undefined) {
|
|
node.setLanguage(tokenizer.defaultLanguage);
|
|
}
|
|
const language = node.getLanguage() || tokenizer.defaultLanguage;
|
|
if (isCodeLanguageLoaded(language)) {
|
|
if (!node.getIsSyntaxHighlightSupported()) {
|
|
node.setIsSyntaxHighlightSupported(true);
|
|
}
|
|
} else {
|
|
if (node.getIsSyntaxHighlightSupported()) {
|
|
node.setIsSyntaxHighlightSupported(false);
|
|
}
|
|
loadCodeLanguage(language, editor, nodeKey);
|
|
return;
|
|
}
|
|
if (nodesCurrentlyHighlighting.has(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(() => {
|
|
$updateAndRetainSelection(nodeKey, () => {
|
|
const currentNode = lexical.$getNodeByKey(nodeKey);
|
|
if (!$isCodeNode(currentNode) || !currentNode.isAttached()) {
|
|
return false;
|
|
}
|
|
//const DIFF_LANGUAGE_REGEX = /^diff-([\w-]+)/i;
|
|
const currentLanguage = currentNode.getLanguage() || tokenizer.defaultLanguage;
|
|
//const diffLanguageMatch = DIFF_LANGUAGE_REGEX.exec(currentLanguage);
|
|
|
|
const highlightNodes = tokenizer.$tokenize(currentNode, currentLanguage);
|
|
const diffRange = getDiffRange(currentNode.getChildren(), highlightNodes);
|
|
const {
|
|
from,
|
|
to,
|
|
nodesForReplacement
|
|
} = diffRange;
|
|
if (from !== to || nodesForReplacement.length) {
|
|
node.splice(from, to - from, nodesForReplacement);
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}, {
|
|
onUpdate: () => {
|
|
nodesCurrentlyHighlighting.delete(nodeKey);
|
|
},
|
|
skipTransforms: true
|
|
});
|
|
}
|
|
|
|
// Wrapping update function into selection retainer, that tries to keep cursor at the same
|
|
// position as before.
|
|
function $updateAndRetainSelection(nodeKey, updateFn) {
|
|
const node = lexical.$getNodeByKey(nodeKey);
|
|
if (!$isCodeNode(node) || !node.isAttached()) {
|
|
return;
|
|
}
|
|
const selection = lexical.$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 (!lexical.$isRangeSelection(selection)) {
|
|
updateFn();
|
|
return;
|
|
}
|
|
const anchor = selection.anchor;
|
|
const anchorOffset = anchor.offset;
|
|
const isNewLineAnchor = anchor.type === 'element' && lexical.$isLineBreakNode(node.getChildAtIndex(anchor.offset - 1));
|
|
let textOffset = 0;
|
|
|
|
// Calculating previous text offset (all text node prior to anchor + anchor own text offset)
|
|
if (!isNewLineAnchor) {
|
|
const anchorNode = anchor.getNode();
|
|
textOffset = anchorOffset + anchorNode.getPreviousSiblings().reduce((offset, _node) => {
|
|
return offset + _node.getTextContentSize();
|
|
}, 0);
|
|
}
|
|
const 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(_node => {
|
|
const isText = lexical.$isTextNode(_node);
|
|
if (isText || lexical.$isLineBreakNode(_node)) {
|
|
const 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) {
|
|
let leadingMatch = 0;
|
|
while (leadingMatch < prevNodes.length) {
|
|
if (!isEqual(prevNodes[leadingMatch], nextNodes[leadingMatch])) {
|
|
break;
|
|
}
|
|
leadingMatch++;
|
|
}
|
|
const prevNodesLength = prevNodes.length;
|
|
const nextNodesLength = nextNodes.length;
|
|
const maxTrailingMatch = Math.min(prevNodesLength, nextNodesLength) - leadingMatch;
|
|
let trailingMatch = 0;
|
|
while (trailingMatch < maxTrailingMatch) {
|
|
trailingMatch++;
|
|
if (!isEqual(prevNodes[prevNodesLength - trailingMatch], nextNodes[nextNodesLength - trailingMatch])) {
|
|
trailingMatch--;
|
|
break;
|
|
}
|
|
}
|
|
const from = leadingMatch;
|
|
const to = prevNodesLength - trailingMatch;
|
|
const nodesForReplacement = nextNodes.slice(leadingMatch, nextNodesLength - trailingMatch);
|
|
return {
|
|
from,
|
|
nodesForReplacement,
|
|
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 || lexical.$isTabNode(nodeA) && lexical.$isTabNode(nodeB) || lexical.$isLineBreakNode(nodeA) && lexical.$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 (!lexical.$isRangeSelection(selection)) {
|
|
return false;
|
|
}
|
|
const anchorNode = selection.anchor.getNode();
|
|
const maybeAnchorCodeNode = $isCodeNode(anchorNode) ? anchorNode : anchorNode.getParent();
|
|
const focusNode = selection.focus.getNode();
|
|
const 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) {
|
|
const nodes = selection.getNodes();
|
|
const lines = [];
|
|
if (nodes.length === 1 && $isCodeNode(nodes[0])) {
|
|
return lines;
|
|
}
|
|
let lastLine = [];
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const node = nodes[i];
|
|
if (!($isCodeHighlightNode(node) || lexical.$isTabNode(node) || lexical.$isLineBreakNode(node))) {
|
|
formatDevErrorMessage(`Expected selection to be inside CodeBlock and consisting of CodeHighlightNode, TabNode and LineBreakNode`);
|
|
}
|
|
if (lexical.$isLineBreakNode(node)) {
|
|
if (lastLine.length > 0) {
|
|
lines.push(lastLine);
|
|
lastLine = [];
|
|
}
|
|
} else {
|
|
lastLine.push(node);
|
|
}
|
|
}
|
|
if (lastLine.length > 0) {
|
|
const 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)
|
|
const lastPoint = lexical.$createPoint(lastLine[0].getKey(), 0, 'text');
|
|
if (!selectionEnd.is(lastPoint)) {
|
|
lines.push(lastLine);
|
|
}
|
|
}
|
|
return lines;
|
|
}
|
|
function $handleTab(shiftKey) {
|
|
const selection = lexical.$getSelection();
|
|
if (!lexical.$isRangeSelection(selection) || !$isSelectionInCode(selection)) {
|
|
return null;
|
|
}
|
|
const indentOrOutdent = !shiftKey ? lexical.INDENT_CONTENT_COMMAND : lexical.OUTDENT_CONTENT_COMMAND;
|
|
const tabOrOutdent = !shiftKey ? lexical.INSERT_TAB_COMMAND : lexical.OUTDENT_CONTENT_COMMAND;
|
|
const anchor = selection.anchor;
|
|
const 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
|
|
const codeLines = $getCodeLines(selection);
|
|
if (codeLines.length !== 1) {
|
|
return indentOrOutdent;
|
|
}
|
|
const codeLine = codeLines[0];
|
|
const codeLineLength = codeLine.length;
|
|
if (!(codeLineLength !== 0)) {
|
|
formatDevErrorMessage(`$getCodeLines only extracts non-empty lines`);
|
|
} // Take into account the direction of the selection
|
|
let selectionFirst;
|
|
let 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
|
|
const firstOfLine = $getFirstCodeNodeOfLine(codeLine[0]);
|
|
const lastOfLine = $getLastCodeNodeOfLine(codeLine[0]);
|
|
const anchorOfLine = lexical.$createPoint(firstOfLine.getKey(), 0, 'text');
|
|
const focusOfLine = lexical.$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) {
|
|
const selection = lexical.$getSelection();
|
|
if (!lexical.$isRangeSelection(selection) || !$isSelectionInCode(selection)) {
|
|
return false;
|
|
}
|
|
const codeLines = $getCodeLines(selection);
|
|
const codeLinesLength = codeLines.length;
|
|
|
|
// Special Indent case
|
|
// Selection is collapsed at the beginning of a line
|
|
if (codeLinesLength === 0 && selection.isCollapsed()) {
|
|
if (type === lexical.INDENT_CONTENT_COMMAND) {
|
|
selection.insertNodes([lexical.$createTabNode()]);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Special Indent case
|
|
// Selection is matching only one LineBreak
|
|
if (codeLinesLength === 0 && type === lexical.INDENT_CONTENT_COMMAND && selection.getTextContent() === '\n') {
|
|
const tabNode = lexical.$createTabNode();
|
|
const lineBreakNode = lexical.$createLineBreakNode();
|
|
const direction = selection.isBackward() ? 'previous' : 'next';
|
|
selection.insertNodes([tabNode, lineBreakNode]);
|
|
lexical.$setSelectionFromCaretRange(lexical.$getCaretRangeInDirection(lexical.$getCaretRange(lexical.$getTextPointCaret(tabNode, 'next', 0), lexical.$normalizeCaret(lexical.$getSiblingCaret(lineBreakNode, 'next'))), direction));
|
|
return true;
|
|
}
|
|
|
|
// Indent Non Empty Lines
|
|
for (let i = 0; i < codeLinesLength; i++) {
|
|
const line = codeLines[i];
|
|
// a line here is never empty
|
|
if (line.length > 0) {
|
|
let 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 === lexical.INDENT_CONTENT_COMMAND) {
|
|
const tabNode = lexical.$createTabNode();
|
|
firstOfLine.insertBefore(tabNode);
|
|
// First real code line may need selection adjustment
|
|
// when firstOfLine is at the selection boundary
|
|
if (i === 0) {
|
|
const anchorKey = selection.isBackward() ? 'focus' : 'anchor';
|
|
const anchorLine = lexical.$createPoint(firstOfLine.getKey(), 0, 'text');
|
|
if (selection[anchorKey].is(anchorLine)) {
|
|
selection[anchorKey].set(tabNode.getKey(), 0, 'text');
|
|
}
|
|
}
|
|
} else if (lexical.$isTabNode(firstOfLine)) {
|
|
firstOfLine.remove();
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
function $handleShiftLines(type, event) {
|
|
// We only care about the alt+arrow keys
|
|
const selection = lexical.$getSelection();
|
|
if (!lexical.$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
|
|
const {
|
|
anchor,
|
|
focus
|
|
} = selection;
|
|
const anchorOffset = anchor.offset;
|
|
const focusOffset = focus.offset;
|
|
const anchorNode = anchor.getNode();
|
|
const focusNode = focus.getNode();
|
|
const arrowIsUp = type === lexical.KEY_ARROW_UP_COMMAND;
|
|
|
|
// Ensure the selection is within the codeblock
|
|
if (!$isSelectionInCode(selection) || !($isCodeHighlightNode(anchorNode) || lexical.$isTabNode(anchorNode)) || !($isCodeHighlightNode(focusNode) || lexical.$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()) {
|
|
const codeNode = anchorNode.getParentOrThrow();
|
|
if (arrowIsUp && anchorOffset === 0 && anchorNode.getPreviousSibling() === null) {
|
|
const codeNodeSibling = codeNode.getPreviousSibling();
|
|
if (codeNodeSibling === null) {
|
|
codeNode.selectPrevious();
|
|
event.preventDefault();
|
|
return true;
|
|
}
|
|
} else if (!arrowIsUp && anchorOffset === anchorNode.getTextContentSize() && anchorNode.getNextSibling() === null) {
|
|
const codeNodeSibling = codeNode.getNextSibling();
|
|
if (codeNodeSibling === null) {
|
|
codeNode.selectNext();
|
|
event.preventDefault();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
let start;
|
|
let 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;
|
|
}
|
|
const range = start.getNodesBetween(end);
|
|
for (let i = 0; i < range.length; i++) {
|
|
const node = range[i];
|
|
if (!$isCodeHighlightNode(node) && !lexical.$isTabNode(node) && !lexical.$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
|
|
event.preventDefault();
|
|
event.stopPropagation(); // required to stop cursor movement under Firefox
|
|
|
|
const linebreak = arrowIsUp ? start.getPreviousSibling() : end.getNextSibling();
|
|
if (!lexical.$isLineBreakNode(linebreak)) {
|
|
return true;
|
|
}
|
|
const sibling = arrowIsUp ? linebreak.getPreviousSibling() : linebreak.getNextSibling();
|
|
if (sibling == null) {
|
|
return true;
|
|
}
|
|
const maybeInsertionPoint = $isCodeHighlightNode(sibling) || lexical.$isTabNode(sibling) || lexical.$isLineBreakNode(sibling) ? arrowIsUp ? $getFirstCodeNodeOfLine(sibling) : $getLastCodeNodeOfLine(sibling) : null;
|
|
let insertionPoint = maybeInsertionPoint != null ? maybeInsertionPoint : sibling;
|
|
linebreak.remove();
|
|
range.forEach(node => node.remove());
|
|
if (type === lexical.KEY_ARROW_UP_COMMAND) {
|
|
range.forEach(node => insertionPoint.insertBefore(node));
|
|
insertionPoint.insertBefore(linebreak);
|
|
} else {
|
|
insertionPoint.insertAfter(linebreak);
|
|
insertionPoint = linebreak;
|
|
range.forEach(node => {
|
|
insertionPoint.insertAfter(node);
|
|
insertionPoint = node;
|
|
});
|
|
}
|
|
selection.setTextNodeRange(anchorNode, anchorOffset, focusNode, focusOffset);
|
|
return true;
|
|
}
|
|
function $handleMoveTo(type, event) {
|
|
const selection = lexical.$getSelection();
|
|
if (!lexical.$isRangeSelection(selection)) {
|
|
return false;
|
|
}
|
|
const {
|
|
anchor,
|
|
focus
|
|
} = selection;
|
|
const anchorNode = anchor.getNode();
|
|
const focusNode = focus.getNode();
|
|
const isMoveToStart = type === lexical.MOVE_TO_START;
|
|
|
|
// Ensure the selection is within the codeblock
|
|
if (!$isSelectionInCode(selection) || !($isCodeHighlightNode(anchorNode) || lexical.$isTabNode(anchorNode)) || !($isCodeHighlightNode(focusNode) || lexical.$isTabNode(focusNode))) {
|
|
return false;
|
|
}
|
|
if (isMoveToStart) {
|
|
const start = $getStartOfCodeInLine(focusNode, focus.offset);
|
|
if (start !== null) {
|
|
const {
|
|
node,
|
|
offset
|
|
} = start;
|
|
if (lexical.$isLineBreakNode(node)) {
|
|
node.selectNext(0, 0);
|
|
} else {
|
|
selection.setTextNodeRange(node, offset, node, offset);
|
|
}
|
|
} else {
|
|
focusNode.getParentOrThrow().selectStart();
|
|
}
|
|
} else {
|
|
const node = $getEndOfCodeInLine(focusNode);
|
|
node.select();
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return true;
|
|
}
|
|
function registerCodeHighlighting(editor, tokenizer) {
|
|
if (!editor.hasNodes([CodeNode, CodeHighlightNode])) {
|
|
throw new Error('CodeHighlightPlugin: CodeNode or CodeHighlightNode not registered on editor');
|
|
}
|
|
if (tokenizer == null) {
|
|
tokenizer = PrismTokenizer;
|
|
}
|
|
const registrations = [];
|
|
|
|
// Only register the mutation listener if not in headless mode
|
|
if (editor._headless !== true) {
|
|
registrations.push(editor.registerMutationListener(CodeNode, mutations => {
|
|
editor.update(() => {
|
|
for (const [key, type] of mutations) {
|
|
if (type !== 'destroyed') {
|
|
const node = lexical.$getNodeByKey(key);
|
|
if (node !== null) {
|
|
updateCodeGutter(node, editor);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}, {
|
|
skipInitialization: false
|
|
}));
|
|
}
|
|
|
|
// Add the rest of the registrations
|
|
registrations.push(editor.registerNodeTransform(CodeNode, node => codeNodeTransform(node, editor, tokenizer)), editor.registerNodeTransform(lexical.TextNode, node => $textNodeTransform(node, editor, tokenizer)), editor.registerNodeTransform(CodeHighlightNode, node => $textNodeTransform(node, editor, tokenizer)), editor.registerCommand(lexical.KEY_TAB_COMMAND, event => {
|
|
const command = $handleTab(event.shiftKey);
|
|
if (command === null) {
|
|
return false;
|
|
}
|
|
event.preventDefault();
|
|
editor.dispatchCommand(command, undefined);
|
|
return true;
|
|
}, lexical.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical.INSERT_TAB_COMMAND, () => {
|
|
const selection = lexical.$getSelection();
|
|
if (!$isSelectionInCode(selection)) {
|
|
return false;
|
|
}
|
|
lexical.$insertNodes([lexical.$createTabNode()]);
|
|
return true;
|
|
}, lexical.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical.INDENT_CONTENT_COMMAND, payload => $handleMultilineIndent(lexical.INDENT_CONTENT_COMMAND), lexical.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical.OUTDENT_CONTENT_COMMAND, payload => $handleMultilineIndent(lexical.OUTDENT_CONTENT_COMMAND), lexical.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical.KEY_ARROW_UP_COMMAND, event => {
|
|
const selection = lexical.$getSelection();
|
|
if (!lexical.$isRangeSelection(selection)) {
|
|
return false;
|
|
}
|
|
const {
|
|
anchor
|
|
} = selection;
|
|
const 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(lexical.KEY_ARROW_UP_COMMAND, event);
|
|
}, lexical.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical.KEY_ARROW_DOWN_COMMAND, event => {
|
|
const selection = lexical.$getSelection();
|
|
if (!lexical.$isRangeSelection(selection)) {
|
|
return false;
|
|
}
|
|
const {
|
|
anchor
|
|
} = selection;
|
|
const 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(lexical.KEY_ARROW_DOWN_COMMAND, event);
|
|
}, lexical.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical.MOVE_TO_START, event => $handleMoveTo(lexical.MOVE_TO_START, event), lexical.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical.MOVE_TO_END, event => $handleMoveTo(lexical.MOVE_TO_END, event), lexical.COMMAND_PRIORITY_LOW));
|
|
return utils.mergeRegister(...registrations);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
*/
|
|
|
|
|
|
/** @deprecated renamed to {@link $getFirstCodeNodeOfLine} by @lexical/eslint-plugin rules-of-lexical */
|
|
const getFirstCodeNodeOfLine = $getFirstCodeNodeOfLine;
|
|
/** @deprecated renamed to {@link $getLastCodeNodeOfLine} by @lexical/eslint-plugin rules-of-lexical */
|
|
const getLastCodeNodeOfLine = $getLastCodeNodeOfLine;
|
|
/** @deprecated renamed to {@link $getEndOfCodeInLine} by @lexical/eslint-plugin rules-of-lexical */
|
|
const getEndOfCodeInLine = $getEndOfCodeInLine;
|
|
/** @deprecated renamed to {@link $getStartOfCodeInLine} by @lexical/eslint-plugin rules-of-lexical */
|
|
const getStartOfCodeInLine = $getStartOfCodeInLine;
|
|
|
|
exports.$createCodeHighlightNode = $createCodeHighlightNode;
|
|
exports.$createCodeNode = $createCodeNode;
|
|
exports.$getEndOfCodeInLine = $getEndOfCodeInLine;
|
|
exports.$getFirstCodeNodeOfLine = $getFirstCodeNodeOfLine;
|
|
exports.$getLastCodeNodeOfLine = $getLastCodeNodeOfLine;
|
|
exports.$getStartOfCodeInLine = $getStartOfCodeInLine;
|
|
exports.$isCodeHighlightNode = $isCodeHighlightNode;
|
|
exports.$isCodeNode = $isCodeNode;
|
|
exports.CODE_LANGUAGE_FRIENDLY_NAME_MAP = CODE_LANGUAGE_FRIENDLY_NAME_MAP;
|
|
exports.CODE_LANGUAGE_MAP = CODE_LANGUAGE_MAP;
|
|
exports.CodeHighlightNode = CodeHighlightNode;
|
|
exports.CodeNode = CodeNode;
|
|
exports.DEFAULT_CODE_LANGUAGE = DEFAULT_CODE_LANGUAGE;
|
|
exports.PrismTokenizer = PrismTokenizer;
|
|
exports.getCodeLanguageOptions = getCodeLanguageOptions;
|
|
exports.getCodeLanguages = getCodeLanguages;
|
|
exports.getCodeThemeOptions = getCodeThemeOptions;
|
|
exports.getDefaultCodeLanguage = getDefaultCodeLanguage;
|
|
exports.getEndOfCodeInLine = getEndOfCodeInLine;
|
|
exports.getFirstCodeNodeOfLine = getFirstCodeNodeOfLine;
|
|
exports.getLanguageFriendlyName = getLanguageFriendlyName;
|
|
exports.getLastCodeNodeOfLine = getLastCodeNodeOfLine;
|
|
exports.getStartOfCodeInLine = getStartOfCodeInLine;
|
|
exports.normalizeCodeLang = normalizeCodeLang;
|
|
exports.normalizeCodeLanguage = normalizeCodeLang;
|
|
exports.registerCodeHighlighting = registerCodeHighlighting;
|