@lobehub/editor 4.9.8 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/{ReactSlashPlugin-BZ0TZ1oy.js → ReactSlashPlugin-CBLFF1e6.js} +72 -50
- package/es/headless.js +761 -39
- package/es/index.js +259 -225
- package/es/react.js +1 -1
- package/package.json +1 -1
package/es/headless.js
CHANGED
|
@@ -6,8 +6,8 @@ import EventEmitter from "eventemitter3";
|
|
|
6
6
|
import { $applyNodeReplacement, $caretFromPoint, $createLineBreakNode, $createNodeSelection, $createParagraphNode, $createRangeSelection, $createTextNode, $getCaretRange, $getCharacterOffsets, $getChildCaret, $getNearestNodeFromDOMNode, $getNodeByKey, $getPreviousSelection, $getRoot, $getSelection, $insertNodes, $isBlockElementNode, $isDecoratorNode, $isElementNode, $isLineBreakNode, $isNodeSelection, $isRangeSelection, $isRootNode, $isRootOrShadowRoot, $isTextNode, $isTextPointCaret, $nodesOfType, $normalizeSelection__EXPERIMENTAL, $parseSerializedNode, $setSelection, COLLABORATION_TAG, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, CONTROLLED_TEXT_INSERTION_COMMAND, COPY_COMMAND, DecoratorNode, ElementNode, FORMAT_TEXT_COMMAND, HISTORIC_TAG, HISTORY_MERGE_TAG, HISTORY_PUSH_TAG, INDENT_CONTENT_COMMAND, INSERT_LINE_BREAK_COMMAND, INSERT_PARAGRAPH_COMMAND, INSERT_TAB_COMMAND, IS_BOLD, IS_CODE, IS_ITALIC, IS_STRIKETHROUGH, IS_SUBSCRIPT, IS_SUPERSCRIPT, IS_UNDERLINE, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND, KEY_ENTER_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, PASTE_COMMAND, ParagraphNode, REDO_COMMAND, SELECTION_CHANGE_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, TEXT_TYPE_TO_FORMAT, TabNode, TextNode, UNDO_COMMAND, createCommand, createEditor, getNearestEditorFromDOMNode, isHTMLElement, isModifierMatch, resetRandomKey } from "lexical";
|
|
7
7
|
import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, HeadingNode, QuoteNode, registerRichText } from "@lexical/rich-text";
|
|
8
8
|
import createDebug from "debug";
|
|
9
|
-
import { $filter, $findMatchingParent, $getNearestBlockElementAncestorOrThrow, $getNearestNodeOfType, $insertNodeToNearestRoot, CAN_USE_DOM, IS_APPLE as isApple, addClassNamesToElement, calculateZoomLevel, isHTMLElement as isHTMLElement$1, mergeRegister } from "@lexical/utils";
|
|
10
|
-
import { $setBlocksType } from "@lexical/selection";
|
|
9
|
+
import { $filter, $findMatchingParent, $getNearestBlockElementAncestorOrThrow, $getNearestNodeOfType, $insertNodeToNearestRoot, CAN_USE_DOM, IS_APPLE as isApple, addClassNamesToElement, calculateZoomLevel, isHTMLAnchorElement, isHTMLElement as isHTMLElement$1, mergeRegister } from "@lexical/utils";
|
|
10
|
+
import { $isAtNodeEnd, $setBlocksType } from "@lexical/selection";
|
|
11
11
|
import { registerDragonSupport } from "@lexical/dragon";
|
|
12
12
|
import { $createListItemNode, $createListNode, $insertList, $isListItemNode, $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode, registerList, registerListStrictIndentTransform } from "@lexical/list";
|
|
13
13
|
import { DOMParser as DOMParser$1 } from "@xmldom/xmldom";
|
|
@@ -621,7 +621,7 @@ const isHotkeyMatch = (event, hotkey) => {
|
|
|
621
621
|
};
|
|
622
622
|
//#endregion
|
|
623
623
|
//#region src/utils/hotkey/registerHotkey.ts
|
|
624
|
-
const logger$
|
|
624
|
+
const logger$7 = createDebugLogger("hotkey");
|
|
625
625
|
const registerHotkey = (hotkey, callback, options = {}) => {
|
|
626
626
|
return (e) => {
|
|
627
627
|
if (!isHotkeyMatch(e, hotkey.keys)) return false;
|
|
@@ -633,7 +633,7 @@ const registerHotkey = (hotkey, callback, options = {}) => {
|
|
|
633
633
|
...keys,
|
|
634
634
|
scopes: hotkey.scopes
|
|
635
635
|
});
|
|
636
|
-
logger$
|
|
636
|
+
logger$7.debug(`⌨️ Hotkey matched: ${hotkey.id} [${hotkey.keys}]`, hotkey);
|
|
637
637
|
return true;
|
|
638
638
|
};
|
|
639
639
|
};
|
|
@@ -701,6 +701,10 @@ const noop = () => {};
|
|
|
701
701
|
function createEmptyEditorState() {
|
|
702
702
|
return createEditor().getEditorState();
|
|
703
703
|
}
|
|
704
|
+
function assert(cond, message) {
|
|
705
|
+
if (cond) return;
|
|
706
|
+
throw new Error(message);
|
|
707
|
+
}
|
|
704
708
|
function getNodeKeyFromDOMNode(dom, editor) {
|
|
705
709
|
return dom[`__lexicalKey_${editor._key}`];
|
|
706
710
|
}
|
|
@@ -1378,7 +1382,7 @@ function registerCommands(editor) {
|
|
|
1378
1382
|
}
|
|
1379
1383
|
//#endregion
|
|
1380
1384
|
//#region src/plugins/common/node/cursor.ts
|
|
1381
|
-
const logger$
|
|
1385
|
+
const logger$6 = createDebugLogger("common", "cursor");
|
|
1382
1386
|
var CardLikeElementNode = class extends ElementNode {
|
|
1383
1387
|
isCardLike() {
|
|
1384
1388
|
return true;
|
|
@@ -1417,7 +1421,7 @@ function registerCursorNode(editor) {
|
|
|
1417
1421
|
const needAddCursor = [];
|
|
1418
1422
|
for (const [kClass, nodeMaps] of mutatedNodes) if (DecoratorNode.prototype.isPrototypeOf(kClass.prototype)) for (const [key, mutation] of nodeMaps) {
|
|
1419
1423
|
const node = $getNodeByKey(key);
|
|
1420
|
-
logger$
|
|
1424
|
+
logger$6.debug("🎭 DecoratorNode mutated:", node?.getType(), mutation, node);
|
|
1421
1425
|
if (mutation === "created" && node?.isInline() && node.getNextSibling() === null) needAddCursor.push(node);
|
|
1422
1426
|
}
|
|
1423
1427
|
if (needAddCursor.length > 0) editor.update(() => {
|
|
@@ -1519,7 +1523,7 @@ function registerCursorNode(editor) {
|
|
|
1519
1523
|
$setSelection(sel);
|
|
1520
1524
|
return true;
|
|
1521
1525
|
} catch (error) {
|
|
1522
|
-
logger$
|
|
1526
|
+
logger$6.error("❌ Cursor selection error:", error);
|
|
1523
1527
|
}
|
|
1524
1528
|
else if ($isCursorNode(focusNode)) try {
|
|
1525
1529
|
const { key: anchorKey, offset: anchorOffset, type: anchorType } = selection.anchor;
|
|
@@ -1530,7 +1534,7 @@ function registerCursorNode(editor) {
|
|
|
1530
1534
|
$setSelection(sel);
|
|
1531
1535
|
return true;
|
|
1532
1536
|
} catch (error) {
|
|
1533
|
-
logger$
|
|
1537
|
+
logger$6.error("❌ Cursor navigation error:", error);
|
|
1534
1538
|
}
|
|
1535
1539
|
return false;
|
|
1536
1540
|
}, COMMAND_PRIORITY_HIGH), editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, (event) => {
|
|
@@ -1709,7 +1713,7 @@ function charToId(char) {
|
|
|
1709
1713
|
}
|
|
1710
1714
|
//#endregion
|
|
1711
1715
|
//#region src/plugins/litexml/command/index.ts
|
|
1712
|
-
const logger$
|
|
1716
|
+
const logger$5 = createDebugLogger("plugin", "litexml");
|
|
1713
1717
|
function toArrayXml(litexml) {
|
|
1714
1718
|
return Array.isArray(litexml) ? litexml : [litexml];
|
|
1715
1719
|
}
|
|
@@ -1721,7 +1725,7 @@ function tryParseChild(child, editor) {
|
|
|
1721
1725
|
oldNode
|
|
1722
1726
|
};
|
|
1723
1727
|
} catch (error) {
|
|
1724
|
-
logger$
|
|
1728
|
+
logger$5.error("❌ Error parsing child node:", error);
|
|
1725
1729
|
return {
|
|
1726
1730
|
newNode: null,
|
|
1727
1731
|
oldNode: null
|
|
@@ -1834,12 +1838,12 @@ function registerLiteXMLCommand(editor, dataSource) {
|
|
|
1834
1838
|
delay: true
|
|
1835
1839
|
}, dataSource);
|
|
1836
1840
|
break;
|
|
1837
|
-
default: logger$
|
|
1841
|
+
default: logger$5.warn(`⚠️ Unknown action type: ${action}`);
|
|
1838
1842
|
}
|
|
1839
1843
|
});
|
|
1840
1844
|
return false;
|
|
1841
1845
|
} catch (error) {
|
|
1842
|
-
logger$
|
|
1846
|
+
logger$5.error("❌ Error processing LITEXML_MODIFY_COMMAND:", error);
|
|
1843
1847
|
return false;
|
|
1844
1848
|
}
|
|
1845
1849
|
}, COMMAND_PRIORITY_EDITOR), editor.registerCommand(LITEXML_APPLY_COMMAND, (payload) => {
|
|
@@ -1864,9 +1868,9 @@ function handleModify(editor, dataSource, arrayXml, delay) {
|
|
|
1864
1868
|
try {
|
|
1865
1869
|
const { oldNode, newNode } = tryParseChild(child, editor);
|
|
1866
1870
|
if (oldNode && newNode) handleReplaceForApplyDelay(oldNode, newNode, modifyBlockNodes, diffNodeMap, editor);
|
|
1867
|
-
else logger$
|
|
1871
|
+
else logger$5.warn(`⚠️ Node with key ${child.id} not found for diffing.`);
|
|
1868
1872
|
} catch (error) {
|
|
1869
|
-
logger$
|
|
1873
|
+
logger$5.error("❌ Error replacing node:", error);
|
|
1870
1874
|
}
|
|
1871
1875
|
});
|
|
1872
1876
|
});
|
|
@@ -1890,7 +1894,7 @@ function handleModify(editor, dataSource, arrayXml, delay) {
|
|
|
1890
1894
|
} else prevNode = prevNode.insertAfter(newNode);
|
|
1891
1895
|
else $insertNodes([newNode]);
|
|
1892
1896
|
} catch (error) {
|
|
1893
|
-
logger$
|
|
1897
|
+
logger$5.error("❌ Error replacing node:", error);
|
|
1894
1898
|
}
|
|
1895
1899
|
});
|
|
1896
1900
|
});
|
|
@@ -2027,7 +2031,7 @@ function handleInsert(editor, payload, dataSource) {
|
|
|
2027
2031
|
});
|
|
2028
2032
|
}
|
|
2029
2033
|
} catch (error) {
|
|
2030
|
-
logger$
|
|
2034
|
+
logger$5.error("❌ Error inserting node:", error);
|
|
2031
2035
|
}
|
|
2032
2036
|
});
|
|
2033
2037
|
}
|
|
@@ -2167,7 +2171,7 @@ var LitexmlService = class {
|
|
|
2167
2171
|
};
|
|
2168
2172
|
//#endregion
|
|
2169
2173
|
//#region src/plugins/litexml/data-source/litexml-data-source.ts
|
|
2170
|
-
const logger$
|
|
2174
|
+
const logger$4 = createDebugLogger("plugin", "litexml");
|
|
2171
2175
|
var IXmlWriterContext = class {
|
|
2172
2176
|
createXmlNode(tagName, attributes, textContent) {
|
|
2173
2177
|
return {
|
|
@@ -2195,7 +2199,7 @@ var LitexmlDataSource = class extends DataSource {
|
|
|
2195
2199
|
const xml = this.parseXMLString(litexml);
|
|
2196
2200
|
const inode = this.xmlToLexical(xml);
|
|
2197
2201
|
this.getService?.(INodeService)?.processNodeTree(inode);
|
|
2198
|
-
logger$
|
|
2202
|
+
logger$4.debug("Parsed XML to Lexical State:", inode);
|
|
2199
2203
|
return inode;
|
|
2200
2204
|
}
|
|
2201
2205
|
/**
|
|
@@ -2216,7 +2220,7 @@ var LitexmlDataSource = class extends DataSource {
|
|
|
2216
2220
|
});
|
|
2217
2221
|
editor.setEditorState(newState);
|
|
2218
2222
|
} catch (error) {
|
|
2219
|
-
logger$
|
|
2223
|
+
logger$4.error("Failed to parse XML:", error);
|
|
2220
2224
|
throw error;
|
|
2221
2225
|
}
|
|
2222
2226
|
}
|
|
@@ -2251,7 +2255,7 @@ var LitexmlDataSource = class extends DataSource {
|
|
|
2251
2255
|
return this.lexicalToXML(rootNode);
|
|
2252
2256
|
});
|
|
2253
2257
|
} catch (error) {
|
|
2254
|
-
logger$
|
|
2258
|
+
logger$4.error("Failed to export to XML:", error);
|
|
2255
2259
|
throw error;
|
|
2256
2260
|
}
|
|
2257
2261
|
}
|
|
@@ -22546,7 +22550,7 @@ function remarkMath(options) {
|
|
|
22546
22550
|
}
|
|
22547
22551
|
//#endregion
|
|
22548
22552
|
//#region src/plugins/markdown/utils/logger.ts
|
|
22549
|
-
const logger$
|
|
22553
|
+
const logger$3 = createDebugLogger("plugin", "markdown");
|
|
22550
22554
|
//#endregion
|
|
22551
22555
|
//#region src/plugins/markdown/data-source/markdown/parse.ts
|
|
22552
22556
|
const selfClosingHtmlTags = new Set([
|
|
@@ -22566,8 +22570,9 @@ const selfClosingHtmlTags = new Set([
|
|
|
22566
22570
|
"wbr"
|
|
22567
22571
|
]);
|
|
22568
22572
|
var MarkdownContext = class {
|
|
22569
|
-
constructor(root) {
|
|
22573
|
+
constructor(root, markdown) {
|
|
22570
22574
|
this.root = root;
|
|
22575
|
+
this.markdown = markdown;
|
|
22571
22576
|
this.stack = [];
|
|
22572
22577
|
}
|
|
22573
22578
|
push(html) {
|
|
@@ -22583,15 +22588,36 @@ var MarkdownContext = class {
|
|
|
22583
22588
|
return this.stack.pop();
|
|
22584
22589
|
}
|
|
22585
22590
|
};
|
|
22586
|
-
|
|
22591
|
+
const getNodeRawMarkdown = (node, markdown) => {
|
|
22592
|
+
const start = node.position?.start.offset;
|
|
22593
|
+
const end = node.position?.end.offset;
|
|
22594
|
+
if (typeof start === "number" && typeof end === "number") return markdown.slice(start, end);
|
|
22595
|
+
if ("value" in node && typeof node.value === "string") return node.value;
|
|
22596
|
+
return "";
|
|
22597
|
+
};
|
|
22598
|
+
const createFallbackRawNode = (node, ctx, parentType) => {
|
|
22599
|
+
const raw = getNodeRawMarkdown(node, ctx.markdown);
|
|
22600
|
+
if (!raw) return null;
|
|
22601
|
+
if (parentType === null || parentType === "root") return {
|
|
22602
|
+
...INodeHelper.createParagraph(),
|
|
22603
|
+
children: [INodeHelper.createTextNode(raw)]
|
|
22604
|
+
};
|
|
22605
|
+
return INodeHelper.createTextNode(raw);
|
|
22606
|
+
};
|
|
22607
|
+
function convertMdastToLexical(node, index, ctx, markdownReaders = {}, parentType = null) {
|
|
22587
22608
|
switch (node.type) {
|
|
22588
22609
|
case "text": return INodeHelper.createTextNode(node.value);
|
|
22589
22610
|
default: {
|
|
22611
|
+
if (!markdownReaders[node.type]) return createFallbackRawNode(node, ctx, parentType);
|
|
22590
22612
|
let children = [];
|
|
22591
22613
|
if ("children" in node && Array.isArray(node.children)) {
|
|
22592
22614
|
let htmlStack = [];
|
|
22593
22615
|
children = node.children.reduce((ret, child, index) => {
|
|
22594
22616
|
if (child.type === "html") {
|
|
22617
|
+
if (!markdownReaders["html"]) {
|
|
22618
|
+
ret.push(INodeHelper.createTextNode(child.value));
|
|
22619
|
+
return ret;
|
|
22620
|
+
}
|
|
22595
22621
|
if (child.value.startsWith("<!--") && child.value.endsWith("-->")) return ret;
|
|
22596
22622
|
const tag = child.value.replaceAll(/^<\/?|>$/g, "");
|
|
22597
22623
|
const isEndTag = child.value.startsWith("</");
|
|
@@ -22617,7 +22643,7 @@ function convertMdastToLexical(node, index, ctx, markdownReaders = {}) {
|
|
|
22617
22643
|
const top = ctx.pop();
|
|
22618
22644
|
htmlStack.pop();
|
|
22619
22645
|
if (top?.tag !== tag) {
|
|
22620
|
-
logger$
|
|
22646
|
+
logger$3.warn("HTML tag mismatch:", tag);
|
|
22621
22647
|
ret.push(...top?.children || []);
|
|
22622
22648
|
return ret;
|
|
22623
22649
|
}
|
|
@@ -22653,10 +22679,10 @@ function convertMdastToLexical(node, index, ctx, markdownReaders = {}) {
|
|
|
22653
22679
|
}
|
|
22654
22680
|
if (htmlStack.length > 0) {
|
|
22655
22681
|
const top = ctx.last;
|
|
22656
|
-
if (top) top.children.push(convertMdastToLexical(child, index, ctx, markdownReaders));
|
|
22682
|
+
if (top) top.children.push(convertMdastToLexical(child, index, ctx, markdownReaders, node.type));
|
|
22657
22683
|
return ret;
|
|
22658
22684
|
}
|
|
22659
|
-
ret.push(convertMdastToLexical(child, index, ctx, markdownReaders));
|
|
22685
|
+
ret.push(convertMdastToLexical(child, index, ctx, markdownReaders, node.type));
|
|
22660
22686
|
return ret;
|
|
22661
22687
|
}, []).filter(Boolean).flat();
|
|
22662
22688
|
while (htmlStack.length > 0) {
|
|
@@ -22707,8 +22733,8 @@ function registerDefaultReaders(markdownReaders) {
|
|
|
22707
22733
|
}
|
|
22708
22734
|
function parseMarkdownToLexical(markdown, markdownReaders = {}) {
|
|
22709
22735
|
const ast = remark().use(remarkCjkFriendly).use(remarkMath).use([[remarkGfm, { singleTilde: false }]]).parse(markdown);
|
|
22710
|
-
logger$
|
|
22711
|
-
const ctx = new MarkdownContext(ast);
|
|
22736
|
+
logger$3.debug("Parsed MDAST:", ast);
|
|
22737
|
+
const ctx = new MarkdownContext(ast, markdown);
|
|
22712
22738
|
registerDefaultReaders(markdownReaders);
|
|
22713
22739
|
return convertMdastToLexical(ast, 0, ctx, markdownReaders);
|
|
22714
22740
|
}
|
|
@@ -22855,7 +22881,7 @@ function insertIRootNode(editor, root, selection) {
|
|
|
22855
22881
|
}
|
|
22856
22882
|
//#endregion
|
|
22857
22883
|
//#region src/plugins/markdown/command/index.ts
|
|
22858
|
-
const logger$
|
|
22884
|
+
const logger$2 = createDebugLogger("plugin", "markdown");
|
|
22859
22885
|
const INSERT_MARKDOWN_COMMAND = createCommand("INSERT_MARKDOWN_COMMAND");
|
|
22860
22886
|
const GET_MARKDOWN_SELECTION_COMMAND = createCommand("GET_MARKDOWN_SELECTION_COMMAND");
|
|
22861
22887
|
function restoreToEntry(editor, entry) {
|
|
@@ -22869,7 +22895,7 @@ const getLineNumber = (content, charIndex) => {
|
|
|
22869
22895
|
function registerMarkdownCommand(editor, kernel, service) {
|
|
22870
22896
|
return mergeRegister(editor.registerCommand(INSERT_MARKDOWN_COMMAND, (payload) => {
|
|
22871
22897
|
const { markdown } = payload;
|
|
22872
|
-
logger$
|
|
22898
|
+
logger$2.debug("INSERT_MARKDOWN_COMMAND payload:", payload);
|
|
22873
22899
|
restoreToEntry(editor, payload.historyState);
|
|
22874
22900
|
setTimeout(() => {
|
|
22875
22901
|
editor.update(() => {
|
|
@@ -22877,11 +22903,11 @@ function registerMarkdownCommand(editor, kernel, service) {
|
|
|
22877
22903
|
const root = parseMarkdownToLexical(markdown, service.markdownReaders);
|
|
22878
22904
|
const selection = $getSelection();
|
|
22879
22905
|
const nodes = $generateNodesFromSerializedNodes(root.children);
|
|
22880
|
-
logger$
|
|
22906
|
+
logger$2.debug("INSERT_MARKDOWN_COMMAND nodes:", nodes);
|
|
22881
22907
|
$insertGeneratedNodes(editor, nodes, selection);
|
|
22882
22908
|
return true;
|
|
22883
22909
|
} catch (error) {
|
|
22884
|
-
logger$
|
|
22910
|
+
logger$2.error("Failed to handle markdown paste:", error);
|
|
22885
22911
|
}
|
|
22886
22912
|
}, { tag: HISTORY_PUSH_TAG });
|
|
22887
22913
|
}, 0);
|
|
@@ -22912,8 +22938,8 @@ function registerMarkdownCommand(editor, kernel, service) {
|
|
|
22912
22938
|
const startLine = getLineNumber(markdownContent, startIndex);
|
|
22913
22939
|
const endLine = getLineNumber(markdownContent, endIndex);
|
|
22914
22940
|
payload.onResult(startLine, endLine);
|
|
22915
|
-
logger$
|
|
22916
|
-
logger$
|
|
22941
|
+
logger$2.debug("GET_MARKDOWN_SELECTION_COMMAND markdownContent:", markdownContent);
|
|
22942
|
+
logger$2.debug("GET_MARKDOWN_SELECTION_COMMAND startLine:", startLine, "endLine:", endLine);
|
|
22917
22943
|
return markdownContent;
|
|
22918
22944
|
} });
|
|
22919
22945
|
}
|
|
@@ -22973,7 +22999,7 @@ var MarkdownDataSource = class extends DataSource {
|
|
|
22973
22999
|
}).processSync(markdown);
|
|
22974
23000
|
return String(result);
|
|
22975
23001
|
} catch (error) {
|
|
22976
|
-
logger$
|
|
23002
|
+
logger$3.error("Failed to format markdown:", error);
|
|
22977
23003
|
return markdown;
|
|
22978
23004
|
}
|
|
22979
23005
|
}
|
|
@@ -22986,7 +23012,7 @@ var MarkdownDataSource = class extends DataSource {
|
|
|
22986
23012
|
read(editor, data) {
|
|
22987
23013
|
const inode = { root: parseMarkdownToLexical(data, this.markdownService.markdownReaders) };
|
|
22988
23014
|
this.getService?.(INodeService)?.processNodeTree(inode);
|
|
22989
|
-
logger$
|
|
23015
|
+
logger$3.debug("Parsed Lexical State:", inode);
|
|
22990
23016
|
editor.setEditorState(editor.parseEditorState(inode));
|
|
22991
23017
|
}
|
|
22992
23018
|
write(editor, options) {
|
|
@@ -24401,7 +24427,7 @@ function runPasteHandlers(ctx, handlers) {
|
|
|
24401
24427
|
}
|
|
24402
24428
|
//#endregion
|
|
24403
24429
|
//#region src/plugins/common/plugin/register.ts
|
|
24404
|
-
const logger = createDebugLogger("plugin", "common");
|
|
24430
|
+
const logger$1 = createDebugLogger("plugin", "common");
|
|
24405
24431
|
function resolveElement(element, isBackward, focusOffset) {
|
|
24406
24432
|
const parent = element.getParent();
|
|
24407
24433
|
let offset = focusOffset;
|
|
@@ -24502,7 +24528,7 @@ function registerRichKeydown(editor, kernel, options) {
|
|
|
24502
24528
|
selection.insertText(text);
|
|
24503
24529
|
});
|
|
24504
24530
|
} catch (error) {
|
|
24505
|
-
logger.error("❌ Failed to paste as plain text:", error);
|
|
24531
|
+
logger$1.error("❌ Failed to paste as plain text:", error);
|
|
24506
24532
|
}
|
|
24507
24533
|
}, {
|
|
24508
24534
|
enabled: enableHotkey,
|
|
@@ -24760,7 +24786,7 @@ const CommonPlugin = class extends KernelPlugin {
|
|
|
24760
24786
|
const breakMark = (this.config?.markdownOption ?? true) !== false ? "\n\n" : "\n";
|
|
24761
24787
|
const formats = this.formats;
|
|
24762
24788
|
if (formats.quote) markdownService.registerMarkdownShortCut({
|
|
24763
|
-
regExp:
|
|
24789
|
+
regExp: /^(>|》)\s/,
|
|
24764
24790
|
replace: (parentNode, children, _match, isImport) => {
|
|
24765
24791
|
if (isImport) {
|
|
24766
24792
|
const previousNode = parentNode.getPreviousSibling();
|
|
@@ -25307,6 +25333,701 @@ const HRPlugin = class extends KernelPlugin {
|
|
|
25307
25333
|
}
|
|
25308
25334
|
};
|
|
25309
25335
|
//#endregion
|
|
25336
|
+
//#region src/plugins/link/node/LinkNode.ts
|
|
25337
|
+
const logger = createDebugLogger("plugin", "link");
|
|
25338
|
+
const SUPPORTED_URL_PROTOCOLS$1 = new Set([
|
|
25339
|
+
"http:",
|
|
25340
|
+
"https:",
|
|
25341
|
+
"mailto:",
|
|
25342
|
+
"sms:",
|
|
25343
|
+
"tel:"
|
|
25344
|
+
]);
|
|
25345
|
+
const HOVER_LINK_COMMAND = createCommand("HOVER_LINK_COMMAND");
|
|
25346
|
+
const HOVER_OUT_LINK_COMMAND = createCommand("HOVER_OUT_LINK_COMMAND");
|
|
25347
|
+
/** @noInheritDoc */
|
|
25348
|
+
var LinkNode = class LinkNode extends ElementNode {
|
|
25349
|
+
static getType() {
|
|
25350
|
+
return "link";
|
|
25351
|
+
}
|
|
25352
|
+
static clone(node) {
|
|
25353
|
+
return new LinkNode(node.__url, {
|
|
25354
|
+
rel: node.__rel,
|
|
25355
|
+
target: node.__target,
|
|
25356
|
+
title: node.__title
|
|
25357
|
+
}, node.__key);
|
|
25358
|
+
}
|
|
25359
|
+
constructor(url = "", attributes = {}, key) {
|
|
25360
|
+
super(key);
|
|
25361
|
+
const { target = null, rel = null, title = null } = attributes;
|
|
25362
|
+
this.__url = url;
|
|
25363
|
+
this.__target = target;
|
|
25364
|
+
this.__rel = rel;
|
|
25365
|
+
this.__title = title;
|
|
25366
|
+
}
|
|
25367
|
+
createDOM(config, editor) {
|
|
25368
|
+
logger.debug("🔍 config", config);
|
|
25369
|
+
const element = document.createElement("a");
|
|
25370
|
+
this.updateLinkDOM(null, element, config);
|
|
25371
|
+
addClassNamesToElement(element, config.theme.link);
|
|
25372
|
+
element.addEventListener("mouseenter", (event) => {
|
|
25373
|
+
if (event.target instanceof HTMLElement) {
|
|
25374
|
+
event.target.classList.add("hover");
|
|
25375
|
+
editor.dispatchCommand(HOVER_LINK_COMMAND, {
|
|
25376
|
+
event,
|
|
25377
|
+
linkNode: this
|
|
25378
|
+
});
|
|
25379
|
+
}
|
|
25380
|
+
});
|
|
25381
|
+
element.addEventListener("mouseleave", (event) => {
|
|
25382
|
+
if (event.target instanceof HTMLElement) {
|
|
25383
|
+
event.target.classList.remove("hover");
|
|
25384
|
+
editor.dispatchCommand(HOVER_OUT_LINK_COMMAND, { event });
|
|
25385
|
+
}
|
|
25386
|
+
});
|
|
25387
|
+
return element;
|
|
25388
|
+
}
|
|
25389
|
+
updateLinkDOM(prevNode, anchor, _config) {
|
|
25390
|
+
if (isHTMLAnchorElement(anchor)) {
|
|
25391
|
+
if (!prevNode || prevNode.__url !== this.__url) anchor.href = this.sanitizeUrl(this.__url);
|
|
25392
|
+
for (const attr of [
|
|
25393
|
+
"target",
|
|
25394
|
+
"rel",
|
|
25395
|
+
"title"
|
|
25396
|
+
]) {
|
|
25397
|
+
const key = `__${attr}`;
|
|
25398
|
+
const value = this[key];
|
|
25399
|
+
if (!prevNode || prevNode[key] !== value) if (value) anchor[attr] = value;
|
|
25400
|
+
else anchor.removeAttribute(attr);
|
|
25401
|
+
}
|
|
25402
|
+
}
|
|
25403
|
+
}
|
|
25404
|
+
updateDOM(prevNode, anchor, config) {
|
|
25405
|
+
this.updateLinkDOM(prevNode, anchor, config);
|
|
25406
|
+
return false;
|
|
25407
|
+
}
|
|
25408
|
+
static importDOM() {
|
|
25409
|
+
return { a: () => ({
|
|
25410
|
+
conversion: $convertAnchorElement,
|
|
25411
|
+
priority: 1
|
|
25412
|
+
}) };
|
|
25413
|
+
}
|
|
25414
|
+
static importJSON(serializedNode) {
|
|
25415
|
+
return $createLinkNode().updateFromJSON(serializedNode);
|
|
25416
|
+
}
|
|
25417
|
+
updateFromJSON(serializedNode) {
|
|
25418
|
+
return super.updateFromJSON(serializedNode).setURL(serializedNode.url).setRel(serializedNode.rel || null).setTarget(serializedNode.target || null).setTitle(serializedNode.title || null);
|
|
25419
|
+
}
|
|
25420
|
+
sanitizeUrl(url) {
|
|
25421
|
+
url = formatUrl(url);
|
|
25422
|
+
try {
|
|
25423
|
+
const parsedUrl = new URL(formatUrl(url));
|
|
25424
|
+
if (!SUPPORTED_URL_PROTOCOLS$1.has(parsedUrl.protocol)) return "about:blank";
|
|
25425
|
+
} catch {
|
|
25426
|
+
return url;
|
|
25427
|
+
}
|
|
25428
|
+
return url;
|
|
25429
|
+
}
|
|
25430
|
+
exportJSON() {
|
|
25431
|
+
return {
|
|
25432
|
+
...super.exportJSON(),
|
|
25433
|
+
rel: this.getRel(),
|
|
25434
|
+
target: this.getTarget(),
|
|
25435
|
+
title: this.getTitle(),
|
|
25436
|
+
url: this.getURL()
|
|
25437
|
+
};
|
|
25438
|
+
}
|
|
25439
|
+
getURL() {
|
|
25440
|
+
return this.getLatest().__url;
|
|
25441
|
+
}
|
|
25442
|
+
setURL(url) {
|
|
25443
|
+
const writable = this.getWritable();
|
|
25444
|
+
writable.__url = url;
|
|
25445
|
+
return writable;
|
|
25446
|
+
}
|
|
25447
|
+
getTarget() {
|
|
25448
|
+
return this.getLatest().__target;
|
|
25449
|
+
}
|
|
25450
|
+
setTarget(target) {
|
|
25451
|
+
const writable = this.getWritable();
|
|
25452
|
+
writable.__target = target;
|
|
25453
|
+
return writable;
|
|
25454
|
+
}
|
|
25455
|
+
getRel() {
|
|
25456
|
+
return this.getLatest().__rel;
|
|
25457
|
+
}
|
|
25458
|
+
setRel(rel) {
|
|
25459
|
+
const writable = this.getWritable();
|
|
25460
|
+
writable.__rel = rel;
|
|
25461
|
+
return writable;
|
|
25462
|
+
}
|
|
25463
|
+
getTitle() {
|
|
25464
|
+
return this.getLatest().__title;
|
|
25465
|
+
}
|
|
25466
|
+
setTitle(title) {
|
|
25467
|
+
const writable = this.getWritable();
|
|
25468
|
+
writable.__title = title;
|
|
25469
|
+
return writable;
|
|
25470
|
+
}
|
|
25471
|
+
insertNewAfter(_, restoreSelection = true) {
|
|
25472
|
+
const linkNode = $createLinkNode(this.__url, {
|
|
25473
|
+
rel: this.__rel,
|
|
25474
|
+
target: this.__target,
|
|
25475
|
+
title: this.__title
|
|
25476
|
+
});
|
|
25477
|
+
this.insertAfter(linkNode, restoreSelection);
|
|
25478
|
+
return linkNode;
|
|
25479
|
+
}
|
|
25480
|
+
canInsertTextBefore() {
|
|
25481
|
+
return false;
|
|
25482
|
+
}
|
|
25483
|
+
canInsertTextAfter() {
|
|
25484
|
+
return false;
|
|
25485
|
+
}
|
|
25486
|
+
canBeEmpty() {
|
|
25487
|
+
return false;
|
|
25488
|
+
}
|
|
25489
|
+
isInline() {
|
|
25490
|
+
return true;
|
|
25491
|
+
}
|
|
25492
|
+
extractWithChild(child, selection) {
|
|
25493
|
+
if (!$isRangeSelection(selection)) return false;
|
|
25494
|
+
const anchorNode = selection.anchor.getNode();
|
|
25495
|
+
const focusNode = selection.focus.getNode();
|
|
25496
|
+
return this.isParentOf(anchorNode) && this.isParentOf(focusNode) && selection.getTextContent().length > 0;
|
|
25497
|
+
}
|
|
25498
|
+
isEmailURI() {
|
|
25499
|
+
return this.__url.startsWith("mailto:");
|
|
25500
|
+
}
|
|
25501
|
+
isWebSiteURI() {
|
|
25502
|
+
return this.__url.startsWith("https://") || this.__url.startsWith("http://");
|
|
25503
|
+
}
|
|
25504
|
+
};
|
|
25505
|
+
function $convertAnchorElement(domNode) {
|
|
25506
|
+
let node = null;
|
|
25507
|
+
if (isHTMLAnchorElement(domNode)) {
|
|
25508
|
+
const content = domNode.textContent;
|
|
25509
|
+
if (content !== null && content !== "" || domNode.children.length > 0) node = $createLinkNode(domNode.getAttribute("href") || "", {
|
|
25510
|
+
rel: domNode.getAttribute("rel"),
|
|
25511
|
+
target: domNode.getAttribute("target"),
|
|
25512
|
+
title: domNode.getAttribute("title")
|
|
25513
|
+
});
|
|
25514
|
+
}
|
|
25515
|
+
return { node };
|
|
25516
|
+
}
|
|
25517
|
+
/**
|
|
25518
|
+
* Takes a URL and creates a LinkNode.
|
|
25519
|
+
* @param url - The URL the LinkNode should direct to.
|
|
25520
|
+
* @param attributes - Optional HTML a tag attributes \\{ target, rel, title \\}
|
|
25521
|
+
* @returns The LinkNode.
|
|
25522
|
+
*/
|
|
25523
|
+
function $createLinkNode(url = "", attributes) {
|
|
25524
|
+
return $applyNodeReplacement(new LinkNode(url, attributes));
|
|
25525
|
+
}
|
|
25526
|
+
/**
|
|
25527
|
+
* Determines if node is a LinkNode.
|
|
25528
|
+
* @param node - The node to be checked.
|
|
25529
|
+
* @returns true if node is a LinkNode, false otherwise.
|
|
25530
|
+
*/
|
|
25531
|
+
function $isLinkNode(node) {
|
|
25532
|
+
return node instanceof LinkNode;
|
|
25533
|
+
}
|
|
25534
|
+
var AutoLinkNode = class AutoLinkNode extends LinkNode {
|
|
25535
|
+
constructor(url = "", attributes = {}, key) {
|
|
25536
|
+
super(url, attributes, key);
|
|
25537
|
+
this.__isUnlinked = attributes.isUnlinked !== void 0 && attributes.isUnlinked !== null ? attributes.isUnlinked : false;
|
|
25538
|
+
}
|
|
25539
|
+
static getType() {
|
|
25540
|
+
return "autolink";
|
|
25541
|
+
}
|
|
25542
|
+
static clone(node) {
|
|
25543
|
+
return new AutoLinkNode(node.__url, {
|
|
25544
|
+
isUnlinked: node.__isUnlinked,
|
|
25545
|
+
rel: node.__rel,
|
|
25546
|
+
target: node.__target,
|
|
25547
|
+
title: node.__title
|
|
25548
|
+
}, node.__key);
|
|
25549
|
+
}
|
|
25550
|
+
getIsUnlinked() {
|
|
25551
|
+
return this.__isUnlinked;
|
|
25552
|
+
}
|
|
25553
|
+
setIsUnlinked(value) {
|
|
25554
|
+
const self = this.getWritable();
|
|
25555
|
+
self.__isUnlinked = value;
|
|
25556
|
+
return self;
|
|
25557
|
+
}
|
|
25558
|
+
createDOM(config, editor) {
|
|
25559
|
+
logger.debug("🔍 config", config);
|
|
25560
|
+
if (this.__isUnlinked) return document.createElement("span");
|
|
25561
|
+
else return super.createDOM(config, editor);
|
|
25562
|
+
}
|
|
25563
|
+
updateDOM(prevNode, anchor, config) {
|
|
25564
|
+
return super.updateDOM(prevNode, anchor, config) || prevNode.__isUnlinked !== this.__isUnlinked;
|
|
25565
|
+
}
|
|
25566
|
+
static importJSON(serializedNode) {
|
|
25567
|
+
return $createAutoLinkNode().updateFromJSON(serializedNode);
|
|
25568
|
+
}
|
|
25569
|
+
updateFromJSON(serializedNode) {
|
|
25570
|
+
return super.updateFromJSON(serializedNode).setIsUnlinked(serializedNode.isUnlinked || false);
|
|
25571
|
+
}
|
|
25572
|
+
static importDOM() {
|
|
25573
|
+
return null;
|
|
25574
|
+
}
|
|
25575
|
+
exportJSON() {
|
|
25576
|
+
return {
|
|
25577
|
+
...super.exportJSON(),
|
|
25578
|
+
isUnlinked: this.__isUnlinked
|
|
25579
|
+
};
|
|
25580
|
+
}
|
|
25581
|
+
insertNewAfter(selection, restoreSelection = true) {
|
|
25582
|
+
const element = this.getParentOrThrow().insertNewAfter(selection, restoreSelection);
|
|
25583
|
+
if ($isElementNode(element)) {
|
|
25584
|
+
const linkNode = $createAutoLinkNode(this.__url, {
|
|
25585
|
+
isUnlinked: this.__isUnlinked,
|
|
25586
|
+
rel: this.__rel,
|
|
25587
|
+
target: this.__target,
|
|
25588
|
+
title: this.__title
|
|
25589
|
+
});
|
|
25590
|
+
element.append(linkNode);
|
|
25591
|
+
return linkNode;
|
|
25592
|
+
}
|
|
25593
|
+
return null;
|
|
25594
|
+
}
|
|
25595
|
+
};
|
|
25596
|
+
/**
|
|
25597
|
+
* Takes a URL and creates an AutoLinkNode. AutoLinkNodes are generally automatically generated
|
|
25598
|
+
* during typing, which is especially useful when a button to generate a LinkNode is not practical.
|
|
25599
|
+
* @param url - The URL the LinkNode should direct to.
|
|
25600
|
+
* @param attributes - Optional HTML a tag attributes. \\{ target, rel, title \\}
|
|
25601
|
+
* @returns The LinkNode.
|
|
25602
|
+
*/
|
|
25603
|
+
function $createAutoLinkNode(url = "", attributes) {
|
|
25604
|
+
return $applyNodeReplacement(new AutoLinkNode(url, attributes));
|
|
25605
|
+
}
|
|
25606
|
+
/**
|
|
25607
|
+
* Determines if node is an AutoLinkNode.
|
|
25608
|
+
* @param node - The node to be checked.
|
|
25609
|
+
* @returns true if node is an AutoLinkNode, false otherwise.
|
|
25610
|
+
*/
|
|
25611
|
+
function $isAutoLinkNode(node) {
|
|
25612
|
+
return node instanceof AutoLinkNode;
|
|
25613
|
+
}
|
|
25614
|
+
const TOGGLE_LINK_COMMAND = createCommand("TOGGLE_LINK_COMMAND");
|
|
25615
|
+
function $getPointNode(point, offset) {
|
|
25616
|
+
if (point.type === "element") {
|
|
25617
|
+
const node = point.getNode();
|
|
25618
|
+
assert($isElementNode(node), "$getPointNode: element point is not an ElementNode");
|
|
25619
|
+
return node.getChildren()[point.offset + offset] || null;
|
|
25620
|
+
}
|
|
25621
|
+
return null;
|
|
25622
|
+
}
|
|
25623
|
+
/**
|
|
25624
|
+
* Preserve the logical start/end of a RangeSelection in situations where
|
|
25625
|
+
* the point is an element that may be reparented in the callback.
|
|
25626
|
+
*
|
|
25627
|
+
* @param $fn The function to run
|
|
25628
|
+
* @returns The result of the callback
|
|
25629
|
+
*/
|
|
25630
|
+
function $withSelectedNodes($fn) {
|
|
25631
|
+
const initialSelection = $getSelection();
|
|
25632
|
+
if (!$isRangeSelection(initialSelection)) return $fn();
|
|
25633
|
+
const normalized = $normalizeSelection__EXPERIMENTAL(initialSelection);
|
|
25634
|
+
const isBackwards = normalized.isBackward();
|
|
25635
|
+
const anchorNode = $getPointNode(normalized.anchor, isBackwards ? -1 : 0);
|
|
25636
|
+
const focusNode = $getPointNode(normalized.focus, isBackwards ? 0 : -1);
|
|
25637
|
+
const rval = $fn();
|
|
25638
|
+
if (anchorNode || focusNode) {
|
|
25639
|
+
const updatedSelection = $getSelection();
|
|
25640
|
+
if ($isRangeSelection(updatedSelection)) {
|
|
25641
|
+
const finalSelection = updatedSelection.clone();
|
|
25642
|
+
if (anchorNode) {
|
|
25643
|
+
const anchorParent = anchorNode.getParent();
|
|
25644
|
+
if (anchorParent) finalSelection.anchor.set(anchorParent.getKey(), anchorNode.getIndexWithinParent() + (isBackwards ? 1 : 0), "element");
|
|
25645
|
+
}
|
|
25646
|
+
if (focusNode) {
|
|
25647
|
+
const focusParent = focusNode.getParent();
|
|
25648
|
+
if (focusParent) finalSelection.focus.set(focusParent.getKey(), focusNode.getIndexWithinParent() + (isBackwards ? 0 : 1), "element");
|
|
25649
|
+
}
|
|
25650
|
+
$setSelection($normalizeSelection__EXPERIMENTAL(finalSelection));
|
|
25651
|
+
}
|
|
25652
|
+
}
|
|
25653
|
+
return rval;
|
|
25654
|
+
}
|
|
25655
|
+
/**
|
|
25656
|
+
* Generates or updates a LinkNode. It can also delete a LinkNode if the URL is null,
|
|
25657
|
+
* but saves any children and brings them up to the parent node.
|
|
25658
|
+
* @param url - The URL the link directs to.
|
|
25659
|
+
* @param attributes - Optional HTML a tag attributes. \\{ target, rel, title \\}
|
|
25660
|
+
*/
|
|
25661
|
+
function $toggleLink(url, attributes = {}) {
|
|
25662
|
+
const { target, title } = attributes;
|
|
25663
|
+
const rel = attributes.rel === void 0 ? "noreferrer" : attributes.rel;
|
|
25664
|
+
const selection = $getSelection();
|
|
25665
|
+
if (selection === null || !$isRangeSelection(selection) && !$isNodeSelection(selection)) return;
|
|
25666
|
+
if ($isNodeSelection(selection)) {
|
|
25667
|
+
const nodes = selection.getNodes();
|
|
25668
|
+
if (nodes.length === 0) return;
|
|
25669
|
+
nodes.forEach((node) => {
|
|
25670
|
+
if (url === null) {
|
|
25671
|
+
const linkParent = $findMatchingParent(node, (parent) => !$isAutoLinkNode(parent) && $isLinkNode(parent));
|
|
25672
|
+
if (linkParent) {
|
|
25673
|
+
linkParent.insertBefore(node);
|
|
25674
|
+
if (linkParent.getChildren().length === 0) linkParent.remove();
|
|
25675
|
+
}
|
|
25676
|
+
} else {
|
|
25677
|
+
const existingLink = $findMatchingParent(node, (parent) => !$isAutoLinkNode(parent) && $isLinkNode(parent));
|
|
25678
|
+
if (existingLink) {
|
|
25679
|
+
existingLink.setURL(url);
|
|
25680
|
+
if (target !== void 0) existingLink.setTarget(target);
|
|
25681
|
+
if (rel !== void 0) existingLink.setRel(rel);
|
|
25682
|
+
} else {
|
|
25683
|
+
const linkNode = $createLinkNode(url, {
|
|
25684
|
+
rel,
|
|
25685
|
+
target
|
|
25686
|
+
});
|
|
25687
|
+
node.insertBefore(linkNode);
|
|
25688
|
+
linkNode.append(node);
|
|
25689
|
+
}
|
|
25690
|
+
}
|
|
25691
|
+
});
|
|
25692
|
+
return;
|
|
25693
|
+
}
|
|
25694
|
+
const nodes = selection.extract();
|
|
25695
|
+
if (url === null) {
|
|
25696
|
+
nodes.forEach((node) => {
|
|
25697
|
+
const parentLink = $findMatchingParent(node, (parent) => !$isAutoLinkNode(parent) && $isLinkNode(parent));
|
|
25698
|
+
if (parentLink) {
|
|
25699
|
+
const children = parentLink.getChildren();
|
|
25700
|
+
for (const child of children) parentLink.insertBefore(child);
|
|
25701
|
+
parentLink.remove();
|
|
25702
|
+
}
|
|
25703
|
+
});
|
|
25704
|
+
return;
|
|
25705
|
+
}
|
|
25706
|
+
const updatedNodes = /* @__PURE__ */ new Set();
|
|
25707
|
+
const updateLinkNode = (linkNode) => {
|
|
25708
|
+
if (updatedNodes.has(linkNode.getKey())) return;
|
|
25709
|
+
updatedNodes.add(linkNode.getKey());
|
|
25710
|
+
linkNode.setURL(url);
|
|
25711
|
+
if (target !== void 0) linkNode.setTarget(target);
|
|
25712
|
+
if (rel !== void 0) linkNode.setRel(rel);
|
|
25713
|
+
if (title !== void 0) linkNode.setTitle(title);
|
|
25714
|
+
};
|
|
25715
|
+
if (nodes.length === 1) {
|
|
25716
|
+
const firstNode = nodes[0];
|
|
25717
|
+
const linkNode = $getAncestor(firstNode, $isLinkNode);
|
|
25718
|
+
if (linkNode !== null) return updateLinkNode(linkNode);
|
|
25719
|
+
}
|
|
25720
|
+
$withSelectedNodes(() => {
|
|
25721
|
+
let linkNode = null;
|
|
25722
|
+
for (const node of nodes) {
|
|
25723
|
+
if (!node.isAttached()) continue;
|
|
25724
|
+
const parentLinkNode = $getAncestor(node, $isLinkNode);
|
|
25725
|
+
if (parentLinkNode) {
|
|
25726
|
+
updateLinkNode(parentLinkNode);
|
|
25727
|
+
continue;
|
|
25728
|
+
}
|
|
25729
|
+
if ($isElementNode(node)) {
|
|
25730
|
+
if (!node.isInline()) continue;
|
|
25731
|
+
if ($isLinkNode(node)) {
|
|
25732
|
+
if (!$isAutoLinkNode(node) && (linkNode === null || !linkNode.getParentOrThrow().isParentOf(node))) {
|
|
25733
|
+
updateLinkNode(node);
|
|
25734
|
+
linkNode = node;
|
|
25735
|
+
continue;
|
|
25736
|
+
}
|
|
25737
|
+
for (const child of node.getChildren()) node.insertBefore(child);
|
|
25738
|
+
node.remove();
|
|
25739
|
+
continue;
|
|
25740
|
+
}
|
|
25741
|
+
}
|
|
25742
|
+
const prevLinkNode = node.getPreviousSibling();
|
|
25743
|
+
if ($isLinkNode(prevLinkNode) && prevLinkNode.is(linkNode)) {
|
|
25744
|
+
prevLinkNode.append(node);
|
|
25745
|
+
continue;
|
|
25746
|
+
}
|
|
25747
|
+
linkNode = $createLinkNode(url, {
|
|
25748
|
+
rel,
|
|
25749
|
+
target,
|
|
25750
|
+
title
|
|
25751
|
+
});
|
|
25752
|
+
node.insertAfter(linkNode);
|
|
25753
|
+
linkNode.append(node);
|
|
25754
|
+
}
|
|
25755
|
+
});
|
|
25756
|
+
}
|
|
25757
|
+
function $getAncestor(node, predicate) {
|
|
25758
|
+
let parent = node;
|
|
25759
|
+
while (parent !== null && parent.getParent() !== null && !predicate(parent)) parent = parent.getParentOrThrow();
|
|
25760
|
+
return predicate(parent) ? parent : null;
|
|
25761
|
+
}
|
|
25762
|
+
const PHONE_NUMBER_REGEX = /^\+?[\d\s()-]{5,}$/;
|
|
25763
|
+
/**
|
|
25764
|
+
* Formats a URL string by adding appropriate protocol if missing
|
|
25765
|
+
*
|
|
25766
|
+
* @param url - URL to format
|
|
25767
|
+
* @returns Formatted URL with appropriate protocol
|
|
25768
|
+
*/
|
|
25769
|
+
function formatUrl(url) {
|
|
25770
|
+
if (/^[a-z][\d+.a-z-]*:/i.test(url)) return url;
|
|
25771
|
+
else if (/^[#./]/.test(url)) return url;
|
|
25772
|
+
else if (url.includes("@")) return `mailto:${url}`;
|
|
25773
|
+
else if (PHONE_NUMBER_REGEX.test(url)) return `tel:${url}`;
|
|
25774
|
+
return url;
|
|
25775
|
+
}
|
|
25776
|
+
//#endregion
|
|
25777
|
+
//#region src/plugins/link/command/index.ts
|
|
25778
|
+
const INSERT_LINK_COMMAND = createCommand("INSERT_LINK_COMMAND");
|
|
25779
|
+
const UPDATE_LINK_TEXT_COMMAND = createCommand("UPDATE_LINK_TEXT_COMMAND");
|
|
25780
|
+
function registerLinkCommand(editor) {
|
|
25781
|
+
return mergeRegister(editor.registerCommand(INSERT_LINK_COMMAND, (payload) => {
|
|
25782
|
+
const { url, title = url } = payload;
|
|
25783
|
+
editor.update(() => {
|
|
25784
|
+
const linkNode = $createLinkNode(url, { title });
|
|
25785
|
+
const textNode = $createTextNode(title);
|
|
25786
|
+
linkNode.append(textNode);
|
|
25787
|
+
$insertNodes([linkNode]);
|
|
25788
|
+
});
|
|
25789
|
+
return false;
|
|
25790
|
+
}, COMMAND_PRIORITY_EDITOR), editor.registerCommand(UPDATE_LINK_TEXT_COMMAND, (payload) => {
|
|
25791
|
+
const { key, text } = payload;
|
|
25792
|
+
editor.update(() => {
|
|
25793
|
+
const linkNode = $getNodeByKey(key);
|
|
25794
|
+
if (linkNode) {
|
|
25795
|
+
const newLinkNode = $createLinkNode(linkNode.getURL(), { title: text });
|
|
25796
|
+
const textNode = $createTextNode(text);
|
|
25797
|
+
newLinkNode.append(textNode);
|
|
25798
|
+
linkNode?.replace(newLinkNode);
|
|
25799
|
+
newLinkNode.select(1);
|
|
25800
|
+
}
|
|
25801
|
+
});
|
|
25802
|
+
return false;
|
|
25803
|
+
}, COMMAND_PRIORITY_EDITOR));
|
|
25804
|
+
}
|
|
25805
|
+
//#endregion
|
|
25806
|
+
//#region src/plugins/link/service/i-link-service.ts
|
|
25807
|
+
const ILinkService = genServiceId("LinkService");
|
|
25808
|
+
var LinkService = class extends EventEmitter {
|
|
25809
|
+
constructor(..._args) {
|
|
25810
|
+
super(..._args);
|
|
25811
|
+
this._enableLinkToolbar = true;
|
|
25812
|
+
}
|
|
25813
|
+
get enableLinkToolbar() {
|
|
25814
|
+
return this._enableLinkToolbar;
|
|
25815
|
+
}
|
|
25816
|
+
setLinkToolbar(enable) {
|
|
25817
|
+
this._enableLinkToolbar = enable;
|
|
25818
|
+
this.emit("linkToolbarChange", enable);
|
|
25819
|
+
}
|
|
25820
|
+
};
|
|
25821
|
+
//#endregion
|
|
25822
|
+
//#region src/plugins/link/utils/index.ts
|
|
25823
|
+
const SUPPORTED_URL_PROTOCOLS = new Set([
|
|
25824
|
+
"http:",
|
|
25825
|
+
"https:",
|
|
25826
|
+
"mailto:",
|
|
25827
|
+
"sms:",
|
|
25828
|
+
"tel:"
|
|
25829
|
+
]);
|
|
25830
|
+
function sanitizeUrl(url) {
|
|
25831
|
+
try {
|
|
25832
|
+
const parsedUrl = new URL(url);
|
|
25833
|
+
if (!SUPPORTED_URL_PROTOCOLS.has(parsedUrl.protocol)) return "about:blank";
|
|
25834
|
+
} catch {
|
|
25835
|
+
return url;
|
|
25836
|
+
}
|
|
25837
|
+
return url;
|
|
25838
|
+
}
|
|
25839
|
+
const urlRegExp = /* @__PURE__ */ new RegExp(/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\w$&+,:;=-]+@)?[\d.A-Za-z-]+|(?:www.|[\w$&+,:;=-]+@)[\d.A-Za-z-]+)((?:\/[%+./~\w-_]*)?\??[\w%&+.;=@-]*#?\w*)?)/);
|
|
25840
|
+
function extractUrlFromText(text) {
|
|
25841
|
+
const match = urlRegExp.exec(text);
|
|
25842
|
+
if (!match) return null;
|
|
25843
|
+
const raw = match[0];
|
|
25844
|
+
const start = match.index ?? text.indexOf(raw);
|
|
25845
|
+
const trimmed = raw.replace(/[)\],.;:]+$/u, "");
|
|
25846
|
+
return {
|
|
25847
|
+
index: start,
|
|
25848
|
+
length: trimmed.length,
|
|
25849
|
+
url: trimmed
|
|
25850
|
+
};
|
|
25851
|
+
}
|
|
25852
|
+
function getSelectedNode(selection) {
|
|
25853
|
+
const anchor = selection.anchor;
|
|
25854
|
+
const focus = selection.focus;
|
|
25855
|
+
const anchorNode = selection.anchor.getNode();
|
|
25856
|
+
const focusNode = selection.focus.getNode();
|
|
25857
|
+
if (anchorNode === focusNode) return anchorNode;
|
|
25858
|
+
if (selection.isBackward()) return $isAtNodeEnd(focus) ? anchorNode : focusNode;
|
|
25859
|
+
else return $isAtNodeEnd(anchor) ? anchorNode : focusNode;
|
|
25860
|
+
}
|
|
25861
|
+
//#endregion
|
|
25862
|
+
//#region src/plugins/link/plugin/registry.ts
|
|
25863
|
+
function registerLinkCommands(editor, kernel, options) {
|
|
25864
|
+
const { validateUrl, attributes, enableHotkey = true } = options || {};
|
|
25865
|
+
const state = { isLink: false };
|
|
25866
|
+
const registrations = [editor.registerUpdateListener(() => {
|
|
25867
|
+
const selection = editor.getEditorState().read(() => $getSelection());
|
|
25868
|
+
if (!selection) return;
|
|
25869
|
+
if ($isRangeSelection(selection)) editor.getEditorState().read(() => {
|
|
25870
|
+
const node = getSelectedNode(selection);
|
|
25871
|
+
state.isLink = $isLinkNode(node.getParent()) || $isLinkNode(node);
|
|
25872
|
+
});
|
|
25873
|
+
else state.isLink = false;
|
|
25874
|
+
}), editor.registerCommand(TOGGLE_LINK_COMMAND, (payload) => {
|
|
25875
|
+
if (payload === null) {
|
|
25876
|
+
$toggleLink(payload);
|
|
25877
|
+
return true;
|
|
25878
|
+
} else if (typeof payload === "string") {
|
|
25879
|
+
if (validateUrl === void 0 || validateUrl(payload)) {
|
|
25880
|
+
$toggleLink(payload, attributes);
|
|
25881
|
+
return true;
|
|
25882
|
+
}
|
|
25883
|
+
return false;
|
|
25884
|
+
} else {
|
|
25885
|
+
const { url, target, rel, title } = payload;
|
|
25886
|
+
$toggleLink(url, {
|
|
25887
|
+
...attributes,
|
|
25888
|
+
rel,
|
|
25889
|
+
target,
|
|
25890
|
+
title
|
|
25891
|
+
});
|
|
25892
|
+
return true;
|
|
25893
|
+
}
|
|
25894
|
+
}, COMMAND_PRIORITY_LOW)];
|
|
25895
|
+
registrations.push(kernel.registerHotkey(HotkeyEnum.Link, () => {
|
|
25896
|
+
if (state.isLink) {
|
|
25897
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
25898
|
+
return;
|
|
25899
|
+
}
|
|
25900
|
+
let nextUrl = sanitizeUrl("https://");
|
|
25901
|
+
let expandTo = null;
|
|
25902
|
+
editor.getEditorState().read(() => {
|
|
25903
|
+
const selection = $getSelection();
|
|
25904
|
+
if ($isRangeSelection(selection)) if (!selection.isCollapsed()) {
|
|
25905
|
+
const maybeUrl = formatUrl(selection.getTextContent().trim());
|
|
25906
|
+
if (validateUrl?.(maybeUrl)) nextUrl = maybeUrl;
|
|
25907
|
+
} else {
|
|
25908
|
+
const found = extractUrlFromText(selection.anchor.getNode().getTextContent());
|
|
25909
|
+
if (found && validateUrl?.(formatUrl(found.url))) {
|
|
25910
|
+
nextUrl = formatUrl(found.url);
|
|
25911
|
+
expandTo = {
|
|
25912
|
+
index: found.index,
|
|
25913
|
+
length: found.length
|
|
25914
|
+
};
|
|
25915
|
+
}
|
|
25916
|
+
}
|
|
25917
|
+
});
|
|
25918
|
+
editor.update(() => {
|
|
25919
|
+
if (expandTo) {
|
|
25920
|
+
const selection = $getSelection();
|
|
25921
|
+
if ($isRangeSelection(selection)) {
|
|
25922
|
+
const anchorNode = selection.anchor.getNode();
|
|
25923
|
+
selection.anchor.set(anchorNode.getKey(), expandTo.index, "text");
|
|
25924
|
+
selection.focus.set(anchorNode.getKey(), expandTo.index + expandTo.length, "text");
|
|
25925
|
+
}
|
|
25926
|
+
}
|
|
25927
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, nextUrl);
|
|
25928
|
+
});
|
|
25929
|
+
}, {
|
|
25930
|
+
enabled: enableHotkey,
|
|
25931
|
+
preventDefault: true,
|
|
25932
|
+
stopPropagation: true
|
|
25933
|
+
}));
|
|
25934
|
+
return mergeRegister(...registrations);
|
|
25935
|
+
}
|
|
25936
|
+
//#endregion
|
|
25937
|
+
//#region src/plugins/link/plugin/index.ts
|
|
25938
|
+
const LinkPlugin = class extends KernelPlugin {
|
|
25939
|
+
static {
|
|
25940
|
+
this.pluginName = "LinkPlugin";
|
|
25941
|
+
}
|
|
25942
|
+
constructor(kernel, config) {
|
|
25943
|
+
super();
|
|
25944
|
+
this.kernel = kernel;
|
|
25945
|
+
this.config = config;
|
|
25946
|
+
this.linkRegex = /^https?:\/\/\S+$/;
|
|
25947
|
+
this.service = new LinkService();
|
|
25948
|
+
kernel.registerNodes([LinkNode, AutoLinkNode]);
|
|
25949
|
+
kernel.registerService(ILinkService, this.service);
|
|
25950
|
+
if (config?.theme) kernel.registerThemes(config.theme);
|
|
25951
|
+
if (config?.linkRegex) this.linkRegex = config.linkRegex;
|
|
25952
|
+
}
|
|
25953
|
+
onInit(editor) {
|
|
25954
|
+
this.register(registerLinkCommand(editor));
|
|
25955
|
+
this.register(registerLinkCommands(editor, this.kernel, {
|
|
25956
|
+
attributes: this.config?.attributes,
|
|
25957
|
+
enableHotkey: this.config?.enableHotkey,
|
|
25958
|
+
validateUrl: this.config?.validateUrl
|
|
25959
|
+
}));
|
|
25960
|
+
this.register(editor.registerCommand(PASTE_COMMAND, (payload) => {
|
|
25961
|
+
const { clipboardData } = payload;
|
|
25962
|
+
if (clipboardData && clipboardData.types && clipboardData.types.length === 1 && clipboardData.types[0] === "text/plain") {
|
|
25963
|
+
const data = clipboardData.getData("text/plain").trim();
|
|
25964
|
+
if (this.linkRegex.test(data)) {
|
|
25965
|
+
payload.stopImmediatePropagation();
|
|
25966
|
+
payload.preventDefault();
|
|
25967
|
+
editor.dispatchCommand(INSERT_LINK_COMMAND, { url: data });
|
|
25968
|
+
return true;
|
|
25969
|
+
}
|
|
25970
|
+
}
|
|
25971
|
+
return false;
|
|
25972
|
+
}, COMMAND_PRIORITY_NORMAL));
|
|
25973
|
+
this.registerMarkdown();
|
|
25974
|
+
this.registerLiteXml();
|
|
25975
|
+
}
|
|
25976
|
+
registerLiteXml() {
|
|
25977
|
+
const litexmlService = this.kernel.requireService(ILitexmlService);
|
|
25978
|
+
if (!litexmlService) return;
|
|
25979
|
+
litexmlService.registerXMLWriter(LinkNode.getType(), (node, ctx) => {
|
|
25980
|
+
if ($isLinkNode(node)) {
|
|
25981
|
+
const attributes = { href: node.getURL() };
|
|
25982
|
+
return ctx.createXmlNode("a", attributes);
|
|
25983
|
+
}
|
|
25984
|
+
return false;
|
|
25985
|
+
});
|
|
25986
|
+
litexmlService.registerXMLReader("a", (xmlNode, children) => {
|
|
25987
|
+
return [INodeHelper.createElementNode("link", {
|
|
25988
|
+
children,
|
|
25989
|
+
direction: "ltr",
|
|
25990
|
+
format: "",
|
|
25991
|
+
indent: 0,
|
|
25992
|
+
type: "link",
|
|
25993
|
+
url: xmlNode.getAttribute("href") || "",
|
|
25994
|
+
version: 1
|
|
25995
|
+
})];
|
|
25996
|
+
});
|
|
25997
|
+
}
|
|
25998
|
+
registerMarkdown() {
|
|
25999
|
+
this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownShortCut({
|
|
26000
|
+
regExp: /\[([^[]+)]\(([^\s()]+)(?:\s"((?:[^"]*\\")*[^"]*)"\s*)?\)\s?$/,
|
|
26001
|
+
replace: (textNode, match) => {
|
|
26002
|
+
const [, linkText, linkUrl, linkTitle] = match;
|
|
26003
|
+
const linkNode = $createLinkNode(linkUrl, { title: linkTitle });
|
|
26004
|
+
const linkTextNode = $createTextNode(linkText);
|
|
26005
|
+
linkTextNode.setFormat(textNode.getFormat());
|
|
26006
|
+
linkNode.append(linkTextNode);
|
|
26007
|
+
textNode.replace(linkNode);
|
|
26008
|
+
return linkTextNode;
|
|
26009
|
+
},
|
|
26010
|
+
trigger: ")",
|
|
26011
|
+
type: "text-match"
|
|
26012
|
+
});
|
|
26013
|
+
this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownWriter(LinkNode.getType(), (ctx, node) => {
|
|
26014
|
+
if ($isLinkNode(node)) ctx.wrap("[", `](${node.getURL()})`);
|
|
26015
|
+
});
|
|
26016
|
+
this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownReader("link", (node, children) => {
|
|
26017
|
+
return [INodeHelper.createElementNode("link", {
|
|
26018
|
+
children,
|
|
26019
|
+
direction: "ltr",
|
|
26020
|
+
format: "",
|
|
26021
|
+
indent: 0,
|
|
26022
|
+
title: node.title || void 0,
|
|
26023
|
+
type: "link",
|
|
26024
|
+
url: node.url || "",
|
|
26025
|
+
version: 1
|
|
26026
|
+
})];
|
|
26027
|
+
});
|
|
26028
|
+
}
|
|
26029
|
+
};
|
|
26030
|
+
//#endregion
|
|
25310
26031
|
//#region src/utils/cx.ts
|
|
25311
26032
|
const cx = (...classNames) => classNames.filter(Boolean).join(" ");
|
|
25312
26033
|
//#endregion
|
|
@@ -26113,6 +26834,7 @@ const normalizeLegacyEditorData = (editorData) => {
|
|
|
26113
26834
|
const DEFAULT_HEADLESS_EDITOR_PLUGINS = [
|
|
26114
26835
|
[CommonPlugin, { enableHotkey: false }],
|
|
26115
26836
|
MarkdownPlugin,
|
|
26837
|
+
[LinkPlugin, { enableHotkey: false }],
|
|
26116
26838
|
CodePlugin,
|
|
26117
26839
|
HeadlessCodeblockPlugin,
|
|
26118
26840
|
HRPlugin,
|