@lobehub/editor 1.19.1 → 1.20.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/index.d.ts +2 -0
- package/es/index.js +4 -0
- package/es/plugins/link-highlight/command/index.d.ts +3 -0
- package/es/plugins/link-highlight/command/index.js +46 -0
- package/es/plugins/link-highlight/index.d.ts +4 -0
- package/es/plugins/link-highlight/index.js +5 -0
- package/es/plugins/link-highlight/node/link-highlight.d.ts +29 -0
- package/es/plugins/link-highlight/node/link-highlight.js +223 -0
- package/es/plugins/link-highlight/plugin/index.d.ts +15 -0
- package/es/plugins/link-highlight/plugin/index.js +187 -0
- package/es/plugins/link-highlight/plugin/registry.d.ts +6 -0
- package/es/plugins/link-highlight/plugin/registry.js +61 -0
- package/es/plugins/link-highlight/react/ReactLinkHighlightPlugin.d.ts +4 -0
- package/es/plugins/link-highlight/react/ReactLinkHighlightPlugin.js +35 -0
- package/es/plugins/link-highlight/react/index.d.ts +1 -0
- package/es/plugins/link-highlight/react/index.js +1 -0
- package/es/plugins/link-highlight/react/style.d.ts +3 -0
- package/es/plugins/link-highlight/react/style.js +10 -0
- package/es/plugins/link-highlight/react/type.d.ts +13 -0
- package/es/plugins/link-highlight/react/type.js +1 -0
- package/es/plugins/link-highlight/utils/index.d.ts +17 -0
- package/es/plugins/link-highlight/utils/index.js +43 -0
- package/es/plugins/markdown/index.d.ts +1 -0
- package/es/plugins/markdown/index.js +2 -1
- package/es/plugins/markdown/plugin/index.js +13 -2
- package/es/plugins/markdown/utils/url-validator.d.ts +4 -0
- package/es/plugins/markdown/utils/url-validator.js +6 -0
- package/es/react/hooks/useEditorState/index.js +46 -6
- package/es/utils/url.d.ts +15 -0
- package/es/utils/url.js +51 -0
- package/package.json +1 -1
package/es/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './plugins/file';
|
|
|
7
7
|
export * from './plugins/hr';
|
|
8
8
|
export * from './plugins/image';
|
|
9
9
|
export * from './plugins/link';
|
|
10
|
+
export * from './plugins/link-highlight';
|
|
10
11
|
export * from './plugins/list';
|
|
11
12
|
export * from './plugins/markdown';
|
|
12
13
|
export * from './plugins/math';
|
|
@@ -17,6 +18,7 @@ export * from './plugins/upload';
|
|
|
17
18
|
export type { IEditor } from './types';
|
|
18
19
|
export * from './types/hotkey';
|
|
19
20
|
export { getHotkeyById } from './utils/hotkey/registerHotkey';
|
|
21
|
+
export { isPureUrl, isValidUrl } from './utils/url';
|
|
20
22
|
export { browserDebug, createDebugLogger, debugLogger, debugLoggers, devConsole, prodSafeLogger, } from './utils/debug';
|
|
21
23
|
export { Kernel } from './editor-kernel/kernel';
|
|
22
24
|
/**
|
package/es/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./plugins/file";
|
|
|
7
7
|
export * from "./plugins/hr";
|
|
8
8
|
export * from "./plugins/image";
|
|
9
9
|
export * from "./plugins/link";
|
|
10
|
+
export * from "./plugins/link-highlight";
|
|
10
11
|
export * from "./plugins/list";
|
|
11
12
|
export * from "./plugins/markdown";
|
|
12
13
|
export * from "./plugins/math";
|
|
@@ -18,6 +19,9 @@ export * from "./plugins/upload";
|
|
|
18
19
|
export * from "./types/hotkey";
|
|
19
20
|
export { getHotkeyById } from "./utils/hotkey/registerHotkey";
|
|
20
21
|
|
|
22
|
+
// URL utilities
|
|
23
|
+
export { isPureUrl, isValidUrl } from "./utils/url";
|
|
24
|
+
|
|
21
25
|
// Debug utilities
|
|
22
26
|
export { browserDebug, createDebugLogger, debugLogger, debugLoggers, devConsole, prodSafeLogger } from "./utils/debug";
|
|
23
27
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
|
|
2
|
+
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
3
|
+
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
|
|
4
|
+
import { $getSelection, $insertNodes, $isRangeSelection, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical';
|
|
5
|
+
import { $createCursorNode } from "../../common/node/cursor";
|
|
6
|
+
import { $createLinkHighlightNode, $isLinkHighlightNode, getLinkHighlightNode } from "../node/link-highlight";
|
|
7
|
+
export var INSERT_LINK_HIGHLIGHT_COMMAND = createCommand('INSERT_LINK_HIGHLIGHT_COMMAND');
|
|
8
|
+
export function registerLinkHighlightCommand(editor) {
|
|
9
|
+
return editor.registerCommand(INSERT_LINK_HIGHLIGHT_COMMAND, function () {
|
|
10
|
+
editor.update(function () {
|
|
11
|
+
var selection = $getSelection();
|
|
12
|
+
if (!selection || !$isRangeSelection(selection)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
var focusNode = selection.focus.getNode();
|
|
16
|
+
var anchorNode = selection.anchor.getNode();
|
|
17
|
+
var linkHighlight = getLinkHighlightNode(focusNode);
|
|
18
|
+
if (linkHighlight !== getLinkHighlightNode(anchorNode)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if ($isLinkHighlightNode(linkHighlight)) {
|
|
22
|
+
// Remove link highlight formatting
|
|
23
|
+
var _iterator = _createForOfIteratorHelper(linkHighlight.getChildren().slice(0)),
|
|
24
|
+
_step;
|
|
25
|
+
try {
|
|
26
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
27
|
+
var node = _step.value;
|
|
28
|
+
linkHighlight.insertBefore(node);
|
|
29
|
+
}
|
|
30
|
+
} catch (err) {
|
|
31
|
+
_iterator.e(err);
|
|
32
|
+
} finally {
|
|
33
|
+
_iterator.f();
|
|
34
|
+
}
|
|
35
|
+
linkHighlight.remove();
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create link highlight with selected text
|
|
40
|
+
var linkHighlightNode = $createLinkHighlightNode(selection.getTextContent());
|
|
41
|
+
$insertNodes([linkHighlightNode, $createCursorNode()]);
|
|
42
|
+
linkHighlightNode.select();
|
|
43
|
+
});
|
|
44
|
+
return true;
|
|
45
|
+
}, COMMAND_PRIORITY_EDITOR);
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { EditorConfig, LexicalEditor, LexicalNode, SerializedElementNode } from 'lexical';
|
|
2
|
+
import { CardLikeElementNode } from "../../common/node/cursor";
|
|
3
|
+
export type SerializedLinkHighlightNode = SerializedElementNode;
|
|
4
|
+
export declare class LinkHighlightNode extends CardLikeElementNode {
|
|
5
|
+
static getType(): string;
|
|
6
|
+
static clone(node: LinkHighlightNode): LinkHighlightNode;
|
|
7
|
+
static importJSON(serializedNode: SerializedLinkHighlightNode): LinkHighlightNode;
|
|
8
|
+
createDOM(config: EditorConfig, editor: LexicalEditor): HTMLElement;
|
|
9
|
+
/**
|
|
10
|
+
* Format URL to ensure it has proper protocol
|
|
11
|
+
*/
|
|
12
|
+
private formatUrl;
|
|
13
|
+
getDOMSlot(element: HTMLElement): import("lexical").ElementDOMSlot<HTMLElement>;
|
|
14
|
+
canBeEmpty(): boolean;
|
|
15
|
+
isCardLike(): boolean;
|
|
16
|
+
isInline(): boolean;
|
|
17
|
+
canIndent(): boolean;
|
|
18
|
+
canInsertTextBefore(): boolean;
|
|
19
|
+
canInsertTextAfter(): boolean;
|
|
20
|
+
updateDOM(prevNode: LinkHighlightNode, dom: HTMLElement, config: EditorConfig): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Get the URL from the text content
|
|
23
|
+
*/
|
|
24
|
+
getURL(): string;
|
|
25
|
+
}
|
|
26
|
+
export declare function $createLinkHighlightNode(textContent?: string): LinkHighlightNode;
|
|
27
|
+
export declare function $isLinkHighlightNode(node: unknown): node is LinkHighlightNode;
|
|
28
|
+
export declare function getLinkHighlightNode(node: LexicalNode): LexicalNode | null;
|
|
29
|
+
export declare function $isSelectionInLinkHighlight(editor: LexicalEditor): boolean;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
|
|
2
|
+
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
3
|
+
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
|
|
4
|
+
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
|
|
5
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
|
|
6
|
+
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
7
|
+
function _get() { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get.bind(); } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(arguments.length < 3 ? target : receiver); } return desc.value; }; } return _get.apply(this, arguments); }
|
|
8
|
+
function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = _getPrototypeOf(object); if (object === null) break; } return object; }
|
|
9
|
+
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
|
|
10
|
+
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
|
|
11
|
+
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
|
|
12
|
+
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
|
|
13
|
+
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
|
|
14
|
+
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
|
|
15
|
+
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
|
|
16
|
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
17
|
+
import { addClassNamesToElement } from '@lexical/utils';
|
|
18
|
+
import { $applyNodeReplacement, $createTextNode, $getSelection, $isNodeSelection, $isRangeSelection } from 'lexical';
|
|
19
|
+
import { $createCursorNode, $isCursorNode, CardLikeElementNode } from "../../common/node/cursor";
|
|
20
|
+
export var LinkHighlightNode = /*#__PURE__*/function (_CardLikeElementNode) {
|
|
21
|
+
_inherits(LinkHighlightNode, _CardLikeElementNode);
|
|
22
|
+
var _super = _createSuper(LinkHighlightNode);
|
|
23
|
+
function LinkHighlightNode() {
|
|
24
|
+
_classCallCheck(this, LinkHighlightNode);
|
|
25
|
+
return _super.apply(this, arguments);
|
|
26
|
+
}
|
|
27
|
+
_createClass(LinkHighlightNode, [{
|
|
28
|
+
key: "createDOM",
|
|
29
|
+
value: function createDOM(config, editor) {
|
|
30
|
+
var _this = this;
|
|
31
|
+
var element = document.createElement('span');
|
|
32
|
+
// eslint-disable-next-line unicorn/prefer-dom-node-dataset
|
|
33
|
+
element.setAttribute('data-lexical-key', this.getKey());
|
|
34
|
+
var childContainer = document.createElement('ne-content');
|
|
35
|
+
element.append(childContainer);
|
|
36
|
+
addClassNamesToElement(element, config.theme.linkHighlight);
|
|
37
|
+
|
|
38
|
+
// Add click handler to open link in new window
|
|
39
|
+
element.addEventListener('click', function (event) {
|
|
40
|
+
// Only handle left click without modifier keys
|
|
41
|
+
if (event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey) {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
event.stopPropagation();
|
|
44
|
+
|
|
45
|
+
// Read URL in editor context
|
|
46
|
+
editor.read(function () {
|
|
47
|
+
var url = _this.getURL();
|
|
48
|
+
if (url) {
|
|
49
|
+
// Format URL to ensure it has proper protocol
|
|
50
|
+
var formattedUrl = _this.formatUrl(url);
|
|
51
|
+
window.open(formattedUrl, '_blank', 'noopener,noreferrer');
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return element;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Format URL to ensure it has proper protocol
|
|
61
|
+
*/
|
|
62
|
+
}, {
|
|
63
|
+
key: "formatUrl",
|
|
64
|
+
value: function formatUrl(url) {
|
|
65
|
+
// Check if URL already has a protocol
|
|
66
|
+
if (/^[a-z][\d+.a-z-]*:/i.test(url)) {
|
|
67
|
+
return url;
|
|
68
|
+
}
|
|
69
|
+
// Check if it's a relative path
|
|
70
|
+
if (/^[#./]/.test(url)) {
|
|
71
|
+
return url;
|
|
72
|
+
}
|
|
73
|
+
// Check for email address
|
|
74
|
+
if (url.includes('@') && !url.startsWith('mailto:')) {
|
|
75
|
+
return "mailto:".concat(url);
|
|
76
|
+
}
|
|
77
|
+
// Check for phone number
|
|
78
|
+
if (/^\+?[\d\s()-]{5,}$/.test(url)) {
|
|
79
|
+
return "tel:".concat(url);
|
|
80
|
+
}
|
|
81
|
+
// Default to https
|
|
82
|
+
if (url.startsWith('www.')) {
|
|
83
|
+
return "https://".concat(url);
|
|
84
|
+
}
|
|
85
|
+
// If no protocol, assume https
|
|
86
|
+
if (!url.includes('://')) {
|
|
87
|
+
return "https://".concat(url);
|
|
88
|
+
}
|
|
89
|
+
return url;
|
|
90
|
+
}
|
|
91
|
+
}, {
|
|
92
|
+
key: "getDOMSlot",
|
|
93
|
+
value: function getDOMSlot(element) {
|
|
94
|
+
var neContent = element.querySelector('ne-content');
|
|
95
|
+
if (!neContent) {
|
|
96
|
+
throw new Error('LinkHighlightNode: ne-content not found');
|
|
97
|
+
}
|
|
98
|
+
return _get(_getPrototypeOf(LinkHighlightNode.prototype), "getDOMSlot", this).call(this, element).withElement(neContent);
|
|
99
|
+
}
|
|
100
|
+
}, {
|
|
101
|
+
key: "canBeEmpty",
|
|
102
|
+
value: function canBeEmpty() {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}, {
|
|
106
|
+
key: "isCardLike",
|
|
107
|
+
value: function isCardLike() {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}, {
|
|
111
|
+
key: "isInline",
|
|
112
|
+
value: function isInline() {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
}, {
|
|
116
|
+
key: "canIndent",
|
|
117
|
+
value: function canIndent() {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}, {
|
|
121
|
+
key: "canInsertTextBefore",
|
|
122
|
+
value: function canInsertTextBefore() {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}, {
|
|
126
|
+
key: "canInsertTextAfter",
|
|
127
|
+
value: function canInsertTextAfter() {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}, {
|
|
131
|
+
key: "updateDOM",
|
|
132
|
+
value: function updateDOM(prevNode, dom, config) {
|
|
133
|
+
// Update the class names if theme has changed
|
|
134
|
+
var prevTheme = prevNode ? prevNode : null;
|
|
135
|
+
if (prevTheme !== this) {
|
|
136
|
+
addClassNamesToElement(dom, config.theme.linkHighlight);
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get the URL from the text content
|
|
143
|
+
*/
|
|
144
|
+
}, {
|
|
145
|
+
key: "getURL",
|
|
146
|
+
value: function getURL() {
|
|
147
|
+
return this.getTextContent().trim();
|
|
148
|
+
}
|
|
149
|
+
}], [{
|
|
150
|
+
key: "getType",
|
|
151
|
+
value: function getType() {
|
|
152
|
+
return 'linkHighlight';
|
|
153
|
+
}
|
|
154
|
+
}, {
|
|
155
|
+
key: "clone",
|
|
156
|
+
value: function clone(node) {
|
|
157
|
+
return new LinkHighlightNode(node.__key);
|
|
158
|
+
}
|
|
159
|
+
}, {
|
|
160
|
+
key: "importJSON",
|
|
161
|
+
value: function importJSON(serializedNode) {
|
|
162
|
+
return $createLinkHighlightNode().updateFromJSON(serializedNode);
|
|
163
|
+
}
|
|
164
|
+
}]);
|
|
165
|
+
return LinkHighlightNode;
|
|
166
|
+
}(CardLikeElementNode);
|
|
167
|
+
export function $createLinkHighlightNode(textContent) {
|
|
168
|
+
var linkHighlightNode = $applyNodeReplacement(new LinkHighlightNode());
|
|
169
|
+
var cursorNode = $createCursorNode();
|
|
170
|
+
linkHighlightNode.append(cursorNode);
|
|
171
|
+
if (textContent) {
|
|
172
|
+
linkHighlightNode.append($createTextNode(textContent));
|
|
173
|
+
}
|
|
174
|
+
return linkHighlightNode;
|
|
175
|
+
}
|
|
176
|
+
export function $isLinkHighlightNode(node) {
|
|
177
|
+
return node instanceof LinkHighlightNode;
|
|
178
|
+
}
|
|
179
|
+
export function getLinkHighlightNode(node) {
|
|
180
|
+
if ($isCursorNode(node)) {
|
|
181
|
+
var parent = node.getParent();
|
|
182
|
+
if ($isLinkHighlightNode(parent)) {
|
|
183
|
+
return parent;
|
|
184
|
+
}
|
|
185
|
+
if ($isLinkHighlightNode(node.getNextSibling())) {
|
|
186
|
+
return node.getNextSibling();
|
|
187
|
+
}
|
|
188
|
+
if ($isLinkHighlightNode(node.getPreviousSibling())) {
|
|
189
|
+
return node.getPreviousSibling();
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
if ($isLinkHighlightNode(node.getParent())) {
|
|
194
|
+
return node.getParent();
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
export function $isSelectionInLinkHighlight(editor) {
|
|
199
|
+
return editor.read(function () {
|
|
200
|
+
var selection = $getSelection();
|
|
201
|
+
if (!selection) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
if ($isRangeSelection(selection)) {
|
|
205
|
+
var focusNode = selection.focus.getNode();
|
|
206
|
+
var anchorNode = selection.anchor.getNode();
|
|
207
|
+
var linkHighlight = getLinkHighlightNode(focusNode);
|
|
208
|
+
if (linkHighlight !== getLinkHighlightNode(anchorNode)) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
if ($isLinkHighlightNode(linkHighlight)) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
} else if ($isNodeSelection(selection)) {
|
|
216
|
+
var nodes = selection.getNodes();
|
|
217
|
+
if (nodes.length === 1 && $isLinkHighlightNode(nodes[0])) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IEditorPluginConstructor } from "../../../types";
|
|
2
|
+
export interface LinkHighlightPluginOptions {
|
|
3
|
+
enableHotkey?: boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Enable auto-highlight when pasting URLs
|
|
6
|
+
* @default true
|
|
7
|
+
*/
|
|
8
|
+
enablePasteAutoHighlight?: boolean;
|
|
9
|
+
theme?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Custom URL validation regex
|
|
12
|
+
*/
|
|
13
|
+
urlRegex?: RegExp;
|
|
14
|
+
}
|
|
15
|
+
export declare const LinkHighlightPlugin: IEditorPluginConstructor<LinkHighlightPluginOptions>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
var _class;
|
|
2
|
+
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
|
|
3
|
+
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
|
|
4
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
5
|
+
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
6
|
+
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
|
|
7
|
+
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
8
|
+
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
9
|
+
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
10
|
+
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
|
|
11
|
+
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
|
|
12
|
+
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
|
|
13
|
+
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
|
|
14
|
+
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
|
|
15
|
+
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
|
|
16
|
+
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
|
|
17
|
+
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
|
|
18
|
+
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
|
|
19
|
+
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
20
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
|
|
21
|
+
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
22
|
+
import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_NORMAL, PASTE_COMMAND } from 'lexical';
|
|
23
|
+
import { INodeHelper } from "../../../editor-kernel/inode/helper";
|
|
24
|
+
import { KernelPlugin } from "../../../editor-kernel/plugin";
|
|
25
|
+
import { $createCursorNode, cursorNodeSerialized } from "../../common/node/cursor";
|
|
26
|
+
import { IMarkdownShortCutService, MARKDOWN_READER_LEVEL_HIGH } from "../../markdown/service/shortcut";
|
|
27
|
+
import { createDebugLogger } from "../../../utils/debug";
|
|
28
|
+
import { registerLinkHighlightCommand } from "../command";
|
|
29
|
+
import { $createLinkHighlightNode, LinkHighlightNode } from "../node/link-highlight";
|
|
30
|
+
import { isValidUrl } from "../utils";
|
|
31
|
+
import { registerLinkHighlight } from "./registry";
|
|
32
|
+
export var LinkHighlightPlugin = (_class = /*#__PURE__*/function (_KernelPlugin) {
|
|
33
|
+
_inherits(LinkHighlightPlugin, _KernelPlugin);
|
|
34
|
+
var _super = _createSuper(LinkHighlightPlugin);
|
|
35
|
+
function LinkHighlightPlugin(kernel, config) {
|
|
36
|
+
var _this;
|
|
37
|
+
_classCallCheck(this, LinkHighlightPlugin);
|
|
38
|
+
_this = _super.call(this);
|
|
39
|
+
_defineProperty(_assertThisInitialized(_this), "logger", createDebugLogger('plugin', 'link-highlight'));
|
|
40
|
+
_defineProperty(_assertThisInitialized(_this), "urlRegex", void 0);
|
|
41
|
+
_this.kernel = kernel;
|
|
42
|
+
_this.config = config;
|
|
43
|
+
kernel.registerNodes([LinkHighlightNode]);
|
|
44
|
+
kernel.registerThemes({
|
|
45
|
+
linkHighlight: (config === null || config === void 0 ? void 0 : config.theme) || 'editor-link-highlight'
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Default URL regex that matches http(s), mailto, tel, etc.
|
|
49
|
+
_this.urlRegex = (config === null || config === void 0 ? void 0 : config.urlRegex) || /^(?:https?:\/\/|mailto:|tel:)[^\s"<>[\\\]^`{|}]+|^www\.[^\s"<>[\\\]^`{|}]+/i;
|
|
50
|
+
_this.logger.debug('LinkHighlightPlugin initialized');
|
|
51
|
+
return _this;
|
|
52
|
+
}
|
|
53
|
+
_createClass(LinkHighlightPlugin, [{
|
|
54
|
+
key: "onInit",
|
|
55
|
+
value: function onInit(editor) {
|
|
56
|
+
var _this$config,
|
|
57
|
+
_this$config2,
|
|
58
|
+
_this2 = this;
|
|
59
|
+
this.register(registerLinkHighlightCommand(editor));
|
|
60
|
+
this.register(registerLinkHighlight(editor, this.kernel, {
|
|
61
|
+
enableHotkey: (_this$config = this.config) === null || _this$config === void 0 ? void 0 : _this$config.enableHotkey
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
// Register paste handler for auto-highlighting URLs
|
|
65
|
+
if (((_this$config2 = this.config) === null || _this$config2 === void 0 ? void 0 : _this$config2.enablePasteAutoHighlight) !== false) {
|
|
66
|
+
this.register(editor.registerCommand(PASTE_COMMAND, function (payload) {
|
|
67
|
+
var clipboardData = payload.clipboardData;
|
|
68
|
+
if (clipboardData && clipboardData.types && clipboardData.types.length === 1 && clipboardData.types[0] === 'text/plain') {
|
|
69
|
+
var data = clipboardData.getData('text/plain').trim();
|
|
70
|
+
// Check if the pasted content is a valid URL
|
|
71
|
+
if (_this2.urlRegex.test(data) && isValidUrl(data)) {
|
|
72
|
+
payload.stopImmediatePropagation();
|
|
73
|
+
payload.preventDefault();
|
|
74
|
+
_this2.logger.debug('Auto-highlighting pasted URL:', data);
|
|
75
|
+
|
|
76
|
+
// Insert LinkHighlightNode directly
|
|
77
|
+
editor.update(function () {
|
|
78
|
+
var selection = $getSelection();
|
|
79
|
+
if ($isRangeSelection(selection)) {
|
|
80
|
+
var linkHighlightNode = $createLinkHighlightNode(data);
|
|
81
|
+
var cursorNode = $createCursorNode();
|
|
82
|
+
selection.insertNodes([linkHighlightNode, cursorNode]);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}, COMMAND_PRIORITY_NORMAL));
|
|
90
|
+
}
|
|
91
|
+
var markdownService = this.kernel.requireService(IMarkdownShortCutService);
|
|
92
|
+
if (!markdownService) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Register markdown writer for <link> format
|
|
97
|
+
markdownService.registerMarkdownWriter(LinkHighlightNode.getType(), function (ctx, node) {
|
|
98
|
+
ctx.appendLine("<".concat(node.getTextContent(), ">"));
|
|
99
|
+
return true;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Register markdown shortcut for auto-link syntax: <url>
|
|
103
|
+
// Matches URLs wrapped in angle brackets like <https://example.com>
|
|
104
|
+
markdownService.registerMarkdownShortCut({
|
|
105
|
+
regExp: /<((?:https?:\/\/|mailto:|tel:|www\.)[^\s"<>[\\\]^`{|}]+)>\s?$/,
|
|
106
|
+
replace: function replace(textNode, match) {
|
|
107
|
+
var _match = _slicedToArray(match, 2),
|
|
108
|
+
url = _match[1];
|
|
109
|
+
if (!url || !isValidUrl(url)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
_this2.logger.debug('Converting markdown auto-link to LinkHighlightNode:', url);
|
|
113
|
+
var linkHighlightNode = $createLinkHighlightNode(url);
|
|
114
|
+
var cursorNode = $createCursorNode();
|
|
115
|
+
textNode.replace(linkHighlightNode);
|
|
116
|
+
linkHighlightNode.insertAfter(cursorNode);
|
|
117
|
+
return undefined;
|
|
118
|
+
},
|
|
119
|
+
trigger: '>',
|
|
120
|
+
type: 'text-match'
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Register HTML reader to handle <url> syntax that might be parsed as HTML
|
|
124
|
+
markdownService.registerMarkdownReader('html', function (node) {
|
|
125
|
+
var htmlValue = node.value || '';
|
|
126
|
+
|
|
127
|
+
// Check if this looks like an auto-link: <url>
|
|
128
|
+
var match = htmlValue.match(/^<((?:https?:\/\/|mailto:|tel:|www\.)[^\s"<>[\\\]^`{|}]+)>$/);
|
|
129
|
+
if (match) {
|
|
130
|
+
var url = match[1].replaceAll(/[\u200B-\u200D\u2060\uFEFF]/g, '');
|
|
131
|
+
_this2.logger.debug('Converting HTML auto-link to LinkHighlightNode:', url);
|
|
132
|
+
return [INodeHelper.createElementNode('linkHighlight', {
|
|
133
|
+
children: [cursorNodeSerialized, INodeHelper.createTextNode(url, {})],
|
|
134
|
+
direction: 'ltr',
|
|
135
|
+
format: '',
|
|
136
|
+
indent: 0,
|
|
137
|
+
type: 'linkHighlight',
|
|
138
|
+
version: 1
|
|
139
|
+
}), cursorNodeSerialized];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Not an auto-link, let other handlers process it
|
|
143
|
+
return false;
|
|
144
|
+
}, MARKDOWN_READER_LEVEL_HIGH);
|
|
145
|
+
|
|
146
|
+
// Register markdown reader for 'link' type (auto-links like <url>)
|
|
147
|
+
// Use HIGH priority to handle auto-links before standard Link plugin
|
|
148
|
+
markdownService.registerMarkdownReader('link', function (node, children) {
|
|
149
|
+
// Check if this is an auto-link (URL and text are the same)
|
|
150
|
+
var url = node.url || '';
|
|
151
|
+
|
|
152
|
+
// Check if we have text children
|
|
153
|
+
if (!children || children.length === 0) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Get text content from children
|
|
158
|
+
var textContent = children.filter(function (child) {
|
|
159
|
+
return child.type === 'text';
|
|
160
|
+
}).map(function (child) {
|
|
161
|
+
return child.text || '';
|
|
162
|
+
}).join('');
|
|
163
|
+
|
|
164
|
+
// If text matches URL exactly (auto-link syntax), convert to LinkHighlightNode
|
|
165
|
+
if (textContent === url) {
|
|
166
|
+
_this2.logger.debug('Converting markdown auto-link to LinkHighlightNode:', url);
|
|
167
|
+
// Return array with LinkHighlightNode and trailing cursor
|
|
168
|
+
// Structure matches CodeNode: [node with internal cursor + text, external cursor]
|
|
169
|
+
return [INodeHelper.createElementNode('linkHighlight', {
|
|
170
|
+
children: [cursorNodeSerialized, INodeHelper.createTextNode(url, {})],
|
|
171
|
+
direction: 'ltr',
|
|
172
|
+
format: '',
|
|
173
|
+
indent: 0,
|
|
174
|
+
type: 'linkHighlight',
|
|
175
|
+
version: 1
|
|
176
|
+
}), cursorNodeSerialized];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Otherwise, let standard Link plugin handle it
|
|
180
|
+
return false;
|
|
181
|
+
}, MARKDOWN_READER_LEVEL_HIGH // High priority to intercept auto-links
|
|
182
|
+
);
|
|
183
|
+
this.logger.debug('LinkHighlightPlugin initialized with markdown support');
|
|
184
|
+
}
|
|
185
|
+
}]);
|
|
186
|
+
return LinkHighlightPlugin;
|
|
187
|
+
}(KernelPlugin), _defineProperty(_class, "pluginName", 'LinkHighlightPlugin'), _class);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { LexicalEditor } from 'lexical';
|
|
2
|
+
import { IEditorKernel } from "../../../types";
|
|
3
|
+
export interface LinkHighlightRegistryOptions {
|
|
4
|
+
enableHotkey?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function registerLinkHighlight(editor: LexicalEditor, kernel: IEditorKernel, options?: LinkHighlightRegistryOptions): () => void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
|
|
2
|
+
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
3
|
+
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
|
|
4
|
+
import { mergeRegister } from '@lexical/utils';
|
|
5
|
+
import { $getNodeByKey } from 'lexical';
|
|
6
|
+
import { $createCursorNode } from "../../common/node/cursor";
|
|
7
|
+
import { HotkeyEnum } from "../../../types/hotkey";
|
|
8
|
+
import { INSERT_LINK_HIGHLIGHT_COMMAND } from "../command";
|
|
9
|
+
import { LinkHighlightNode } from "../node/link-highlight";
|
|
10
|
+
export function registerLinkHighlight(editor, kernel, options) {
|
|
11
|
+
var _ref = options || {},
|
|
12
|
+
_ref$enableHotkey = _ref.enableHotkey,
|
|
13
|
+
enableHotkey = _ref$enableHotkey === void 0 ? true : _ref$enableHotkey;
|
|
14
|
+
return mergeRegister(
|
|
15
|
+
// Update listener to ensure cursor node before first LinkHighlightNode
|
|
16
|
+
editor.registerUpdateListener(function (_ref2) {
|
|
17
|
+
var mutatedNodes = _ref2.mutatedNodes;
|
|
18
|
+
var linkHighlightChanged = mutatedNodes === null || mutatedNodes === void 0 ? void 0 : mutatedNodes.get(LinkHighlightNode);
|
|
19
|
+
var keys = (linkHighlightChanged === null || linkHighlightChanged === void 0 ? void 0 : linkHighlightChanged.keys()) || [];
|
|
20
|
+
var needAddBefore = new Set();
|
|
21
|
+
editor.read(function () {
|
|
22
|
+
var _iterator = _createForOfIteratorHelper(keys),
|
|
23
|
+
_step;
|
|
24
|
+
try {
|
|
25
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
26
|
+
var key = _step.value;
|
|
27
|
+
var node = $getNodeByKey(key);
|
|
28
|
+
if (!node) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
var parent = node.getParent();
|
|
32
|
+
if ((parent === null || parent === void 0 ? void 0 : parent.getFirstChild()) === node) {
|
|
33
|
+
needAddBefore.add(node);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
_iterator.e(err);
|
|
38
|
+
} finally {
|
|
39
|
+
_iterator.f();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
if (needAddBefore.size > 0) {
|
|
43
|
+
editor.update(function () {
|
|
44
|
+
needAddBefore.forEach(function (node) {
|
|
45
|
+
var prev = node.getPreviousSibling();
|
|
46
|
+
if (!prev) {
|
|
47
|
+
node.insertBefore($createCursorNode());
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}),
|
|
53
|
+
// Hotkey for toggling link highlight (Ctrl+K or Cmd+K)
|
|
54
|
+
kernel.registerHotkey(HotkeyEnum.Link, function () {
|
|
55
|
+
return editor.dispatchCommand(INSERT_LINK_HIGHLIGHT_COMMAND, undefined);
|
|
56
|
+
}, {
|
|
57
|
+
enabled: enableHotkey,
|
|
58
|
+
preventDefault: true,
|
|
59
|
+
stopPropagation: true
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
|
|
2
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
3
|
+
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
4
|
+
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
|
|
5
|
+
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
6
|
+
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
7
|
+
import { useLayoutEffect } from 'react';
|
|
8
|
+
import { useLexicalComposerContext } from "../../../editor-kernel/react/react-context";
|
|
9
|
+
import { MarkdownPlugin } from "../../markdown/plugin";
|
|
10
|
+
import { LinkHighlightPlugin } from "../plugin";
|
|
11
|
+
import { useStyles } from "./style";
|
|
12
|
+
var ReactLinkHighlightPlugin = function ReactLinkHighlightPlugin(_ref) {
|
|
13
|
+
var className = _ref.className,
|
|
14
|
+
_ref$enableHotkey = _ref.enableHotkey,
|
|
15
|
+
enableHotkey = _ref$enableHotkey === void 0 ? true : _ref$enableHotkey,
|
|
16
|
+
_ref$enablePasteAutoH = _ref.enablePasteAutoHighlight,
|
|
17
|
+
enablePasteAutoHighlight = _ref$enablePasteAutoH === void 0 ? true : _ref$enablePasteAutoH;
|
|
18
|
+
var _useLexicalComposerCo = useLexicalComposerContext(),
|
|
19
|
+
_useLexicalComposerCo2 = _slicedToArray(_useLexicalComposerCo, 1),
|
|
20
|
+
editor = _useLexicalComposerCo2[0];
|
|
21
|
+
var _useStyles = useStyles(),
|
|
22
|
+
cx = _useStyles.cx,
|
|
23
|
+
styles = _useStyles.styles;
|
|
24
|
+
useLayoutEffect(function () {
|
|
25
|
+
editor.registerPlugin(MarkdownPlugin);
|
|
26
|
+
editor.registerPlugin(LinkHighlightPlugin, {
|
|
27
|
+
enableHotkey: enableHotkey,
|
|
28
|
+
enablePasteAutoHighlight: enablePasteAutoHighlight,
|
|
29
|
+
theme: cx(styles.linkHighlight, className)
|
|
30
|
+
});
|
|
31
|
+
}, [className, cx, enableHotkey, enablePasteAutoHighlight, editor, styles.linkHighlight]);
|
|
32
|
+
return null;
|
|
33
|
+
};
|
|
34
|
+
ReactLinkHighlightPlugin.displayName = 'ReactLinkHighlightPlugin';
|
|
35
|
+
export default ReactLinkHighlightPlugin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ReactLinkHighlightPlugin } from './ReactLinkHighlightPlugin';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ReactLinkHighlightPlugin } from "./ReactLinkHighlightPlugin";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
var _templateObject;
|
|
2
|
+
function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); }
|
|
3
|
+
import { createStyles } from 'antd-style';
|
|
4
|
+
export var useStyles = createStyles(function (_ref) {
|
|
5
|
+
var css = _ref.css,
|
|
6
|
+
token = _ref.token;
|
|
7
|
+
return {
|
|
8
|
+
linkHighlight: css(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n cursor: pointer;\n\n display: inline;\n\n padding: 2px;\n\n color: ", ";\n text-decoration: none;\n\n transition: all 0.2s ease;\n\n &:hover {\n color: ", ";\n text-decoration: underline;\n }\n\n ne-content {\n display: inline;\n }\n "])), token.colorLink, token.colorLinkHover)
|
|
9
|
+
};
|
|
10
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ReactLinkHighlightPluginProps {
|
|
2
|
+
className?: string;
|
|
3
|
+
/**
|
|
4
|
+
* Enable keyboard shortcut (Ctrl+K / Cmd+K)
|
|
5
|
+
* @default true
|
|
6
|
+
*/
|
|
7
|
+
enableHotkey?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Enable auto-highlight when pasting URLs
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
12
|
+
enablePasteAutoHighlight?: boolean;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { isValidUrl } from "../../../utils/url";
|
|
2
|
+
/**
|
|
3
|
+
* Sanitizes a URL to ensure it's safe to use
|
|
4
|
+
* @param url - The URL to sanitize
|
|
5
|
+
* @returns The sanitized URL
|
|
6
|
+
*/
|
|
7
|
+
export declare function sanitizeUrl(url: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Extracts URL from text
|
|
10
|
+
* @param text - The text to extract URL from
|
|
11
|
+
* @returns The extracted URL information or null
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractUrlFromText(text: string): {
|
|
14
|
+
index: number;
|
|
15
|
+
length: number;
|
|
16
|
+
url: string;
|
|
17
|
+
} | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Re-export shared URL validation from utils
|
|
2
|
+
export { isValidUrl } from "../../../utils/url";
|
|
3
|
+
|
|
4
|
+
// URL validation regex that matches common URL patterns for extraction
|
|
5
|
+
var urlRegExp = new RegExp(/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\w$&+,:;=-]+@)?[\d.A-Za-z-]+|(?:www.|[\w$&+,:;=-]+@)[\d.A-Za-z-]+)((?:\/[%+./~\w-_]*)?\??[\w%&+.;=@-]*#?\w*)?)/);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sanitizes a URL to ensure it's safe to use
|
|
9
|
+
* @param url - The URL to sanitize
|
|
10
|
+
* @returns The sanitized URL
|
|
11
|
+
*/
|
|
12
|
+
export function sanitizeUrl(url) {
|
|
13
|
+
var SUPPORTED_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'sms:', 'tel:']);
|
|
14
|
+
try {
|
|
15
|
+
var parsedUrl = new URL(url);
|
|
16
|
+
if (!SUPPORTED_URL_PROTOCOLS.has(parsedUrl.protocol)) {
|
|
17
|
+
return 'about:blank';
|
|
18
|
+
}
|
|
19
|
+
} catch (_unused) {
|
|
20
|
+
return url;
|
|
21
|
+
}
|
|
22
|
+
return url;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extracts URL from text
|
|
27
|
+
* @param text - The text to extract URL from
|
|
28
|
+
* @returns The extracted URL information or null
|
|
29
|
+
*/
|
|
30
|
+
export function extractUrlFromText(text) {
|
|
31
|
+
var _index;
|
|
32
|
+
var match = urlRegExp.exec(text);
|
|
33
|
+
if (!match) return null;
|
|
34
|
+
var raw = match[0];
|
|
35
|
+
var start = (_index = match.index) !== null && _index !== void 0 ? _index : text.indexOf(raw);
|
|
36
|
+
// Trim trailing punctuation that often follows inline links
|
|
37
|
+
var trimmed = raw.replace(/[\),\.:;\]]+$/, '');
|
|
38
|
+
return {
|
|
39
|
+
index: start,
|
|
40
|
+
length: trimmed.length,
|
|
41
|
+
url: trimmed
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -5,3 +5,4 @@ export type { MARKDOWN_READER_LEVEL } from './service/shortcut';
|
|
|
5
5
|
export { IMarkdownShortCutService, MARKDOWN_READER_LEVEL_HIGH, MARKDOWN_READER_LEVEL_NORMAL, MARKDOWN_WRITER_LEVEL_MAX, } from './service/shortcut';
|
|
6
6
|
export { isPunctuationChar } from './utils';
|
|
7
7
|
export { detectCodeLanguage, detectLanguage } from './utils/detectLanguage';
|
|
8
|
+
export { isPureUrl, isValidUrl } from './utils/url-validator';
|
|
@@ -3,4 +3,5 @@ export { MarkdownPlugin } from "./plugin";
|
|
|
3
3
|
export { default as ReactMarkdownPlugin } from "./react";
|
|
4
4
|
export { IMarkdownShortCutService, MARKDOWN_READER_LEVEL_HIGH, MARKDOWN_READER_LEVEL_NORMAL, MARKDOWN_WRITER_LEVEL_MAX } from "./service/shortcut";
|
|
5
5
|
export { isPunctuationChar } from "./utils";
|
|
6
|
-
export { detectCodeLanguage, detectLanguage } from "./utils/detectLanguage";
|
|
6
|
+
export { detectCodeLanguage, detectLanguage } from "./utils/detectLanguage";
|
|
7
|
+
export { isPureUrl, isValidUrl } from "./utils/url-validator";
|
|
@@ -23,6 +23,7 @@ import MarkdownDataSource from "../data-source/markdown-data-source";
|
|
|
23
23
|
import { IMarkdownShortCutService, MarkdownShortCutService } from "../service/shortcut";
|
|
24
24
|
import { canContainTransformableMarkdown } from "../utils";
|
|
25
25
|
import { detectCodeLanguage, detectLanguage } from "../utils/detectLanguage";
|
|
26
|
+
import { isValidUrl as isValidLinkUrl } from "../utils/url-validator";
|
|
26
27
|
export var MarkdownPlugin = (_class = /*#__PURE__*/function (_KernelPlugin) {
|
|
27
28
|
_inherits(MarkdownPlugin, _KernelPlugin);
|
|
28
29
|
var _super = _createSuper(MarkdownPlugin);
|
|
@@ -126,8 +127,10 @@ export var MarkdownPlugin = (_class = /*#__PURE__*/function (_KernelPlugin) {
|
|
|
126
127
|
var clipboardData = event.clipboardData;
|
|
127
128
|
if (!clipboardData) return false;
|
|
128
129
|
|
|
129
|
-
// Get clipboard content
|
|
130
|
-
var
|
|
130
|
+
// Get clipboard content and clean BOM/zero-width characters
|
|
131
|
+
var rawText = clipboardData.getData('text/plain').trimEnd();
|
|
132
|
+
// Remove BOM, zero-width spaces, and other invisible characters
|
|
133
|
+
var text = rawText.replaceAll(/[\u200B-\u200D\u2060\uFEFF]/g, '');
|
|
131
134
|
var html = clipboardData.getData('text/html').trimEnd();
|
|
132
135
|
|
|
133
136
|
// If there's no text content, let Lexical handle it
|
|
@@ -206,6 +209,7 @@ export var MarkdownPlugin = (_class = /*#__PURE__*/function (_KernelPlugin) {
|
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
// Check if markdown paste formatting is enabled (default: true)
|
|
212
|
+
// Note: URL pasting is handled by Link/LinkHighlight plugins themselves
|
|
209
213
|
|
|
210
214
|
// Check if content is code (JSON, SQL, etc.) and should be inserted as code block
|
|
211
215
|
var codeInfo = _this2.detectCodeContent(text);
|
|
@@ -270,6 +274,13 @@ export var MarkdownPlugin = (_class = /*#__PURE__*/function (_KernelPlugin) {
|
|
|
270
274
|
return true; // Command handled
|
|
271
275
|
}
|
|
272
276
|
|
|
277
|
+
// Check if the pasted content is a pure URL
|
|
278
|
+
// If so, let Link/LinkHighlight plugins handle it
|
|
279
|
+
if (clipboardData.types.length === 1 && clipboardData.types[0] === 'text/plain' && isValidLinkUrl(text)) {
|
|
280
|
+
_this2.logger.debug('pure URL detected, letting Link/LinkHighlight plugins handle');
|
|
281
|
+
return false; // Let other plugins handle URL paste
|
|
282
|
+
}
|
|
283
|
+
|
|
273
284
|
// Force plain text paste for external content
|
|
274
285
|
event.preventDefault();
|
|
275
286
|
event.stopPropagation();
|
|
@@ -15,6 +15,8 @@ import { INSERT_CODEINLINE_COMMAND } from "../../../plugins/code";
|
|
|
15
15
|
import { $isSelectionInCodeInline } from "../../../plugins/code/node/code";
|
|
16
16
|
import { UPDATE_CODEBLOCK_LANG } from "../../../plugins/codeblock";
|
|
17
17
|
import { $isRootTextContentEmpty } from "../../../plugins/common/utils";
|
|
18
|
+
import { INSERT_LINK_HIGHLIGHT_COMMAND } from "../../../plugins/link-highlight/command";
|
|
19
|
+
import { $isLinkHighlightNode } from "../../../plugins/link-highlight/node/link-highlight";
|
|
18
20
|
import { $isLinkNode, TOGGLE_LINK_COMMAND, formatUrl } from "../../../plugins/link/node/LinkNode";
|
|
19
21
|
import { extractUrlFromText, sanitizeUrl, validateUrl } from "../../../plugins/link/utils";
|
|
20
22
|
import { INSERT_CHECK_LIST_COMMAND } from "../../../plugins/list";
|
|
@@ -124,7 +126,8 @@ export function useEditorState(editor) {
|
|
|
124
126
|
var elementDOM = editor === null || editor === void 0 || (_editor$getLexicalEdi = editor.getLexicalEditor()) === null || _editor$getLexicalEdi === void 0 ? void 0 : _editor$getLexicalEdi.getElementByKey(elementKey);
|
|
125
127
|
var node = getSelectedNode(selection);
|
|
126
128
|
var parent = node.getParent();
|
|
127
|
-
|
|
129
|
+
// Check for both Link and LinkHighlight nodes
|
|
130
|
+
setIsLink($isLinkNode(parent) || $isLinkNode(node) || $isLinkHighlightNode(parent) || $isLinkHighlightNode(node));
|
|
128
131
|
var isCodeBlock = $isCodeNode(element) && $isCodeNode(focusElement) && elementKey === focusElement.getKey();
|
|
129
132
|
setIsInCodeblok(isCodeBlock);
|
|
130
133
|
setCodeblockLang(isCodeBlock ? element.getLanguage() : '');
|
|
@@ -258,7 +261,49 @@ export function useEditorState(editor) {
|
|
|
258
261
|
var insertLink = useCallback(function () {
|
|
259
262
|
var lexical = editor === null || editor === void 0 ? void 0 : editor.getLexicalEditor();
|
|
260
263
|
if (!lexical) return;
|
|
264
|
+
|
|
265
|
+
// Detect which link type we're currently in
|
|
266
|
+
var inLinkNode = false;
|
|
267
|
+
var inLinkHighlightNode = false;
|
|
268
|
+
lexical.getEditorState().read(function () {
|
|
269
|
+
var selection = $getSelection();
|
|
270
|
+
if ($isRangeSelection(selection)) {
|
|
271
|
+
var node = getSelectedNode(selection);
|
|
272
|
+
var parent = node.getParent();
|
|
273
|
+
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
|
274
|
+
inLinkNode = true;
|
|
275
|
+
}
|
|
276
|
+
if ($isLinkHighlightNode(parent) || $isLinkHighlightNode(node)) {
|
|
277
|
+
inLinkHighlightNode = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// If inside LinkHighlightNode, toggle it
|
|
283
|
+
if (inLinkHighlightNode) {
|
|
284
|
+
lexical.dispatchCommand(INSERT_LINK_HIGHLIGHT_COMMAND, undefined);
|
|
285
|
+
setIsLink(false);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// If inside LinkNode, toggle it with standard Link plugin
|
|
290
|
+
if (inLinkNode) {
|
|
291
|
+
setIsLink(false);
|
|
292
|
+
lexical.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Not in any link - try to insert new link
|
|
297
|
+
// Try LinkHighlight first, then fall back to standard Link if not handled
|
|
261
298
|
if (!isLink) {
|
|
299
|
+
// First try INSERT_LINK_HIGHLIGHT_COMMAND
|
|
300
|
+
var linkHighlightHandled = lexical.dispatchCommand(INSERT_LINK_HIGHLIGHT_COMMAND, undefined);
|
|
301
|
+
if (linkHighlightHandled) {
|
|
302
|
+
setIsLink(true);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Fall back to standard Link plugin
|
|
262
307
|
var nextUrl = sanitizeUrl('https://');
|
|
263
308
|
var expandTo = null;
|
|
264
309
|
lexical.getEditorState().read(function () {
|
|
@@ -283,7 +328,6 @@ export function useEditorState(editor) {
|
|
|
283
328
|
}
|
|
284
329
|
});
|
|
285
330
|
setIsLink(true);
|
|
286
|
-
console.log(nextUrl);
|
|
287
331
|
lexical.update(function () {
|
|
288
332
|
if (expandTo) {
|
|
289
333
|
var selection = $getSelection();
|
|
@@ -294,12 +338,8 @@ export function useEditorState(editor) {
|
|
|
294
338
|
selection.focus.set(anchorNode.getKey(), expandTo.index + expandTo.length, 'text');
|
|
295
339
|
}
|
|
296
340
|
}
|
|
297
|
-
console.log(nextUrl);
|
|
298
341
|
lexical.dispatchCommand(TOGGLE_LINK_COMMAND, validateUrl(nextUrl) ? nextUrl : sanitizeUrl('https://'));
|
|
299
342
|
});
|
|
300
|
-
} else {
|
|
301
|
-
setIsLink(false);
|
|
302
|
-
lexical.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
303
343
|
}
|
|
304
344
|
}, [editor, isLink]);
|
|
305
345
|
var insertMath = useCallback(function () {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared URL validation utilities
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Validates if a string is a valid URL
|
|
6
|
+
* @param url - The URL to validate
|
|
7
|
+
* @returns true if the URL is valid, false otherwise
|
|
8
|
+
*/
|
|
9
|
+
export declare function isValidUrl(url: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Checks if text is a pure URL (single URL without other text)
|
|
12
|
+
* @param text - The text to check
|
|
13
|
+
* @returns true if text is a pure URL, false otherwise
|
|
14
|
+
*/
|
|
15
|
+
export declare function isPureUrl(text: string): boolean;
|
package/es/utils/url.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared URL validation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// URL validation regex that matches common URL patterns
|
|
6
|
+
var URL_REGEX = new RegExp(/^(?:(?:https?|ftp):\/\/)?(?:www\.)?[\dA-Za-z][\w#%+./:=?@~-]*[\w#%+/=@~-]$/);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validates if a string is a valid URL
|
|
10
|
+
* @param url - The URL to validate
|
|
11
|
+
* @returns true if the URL is valid, false otherwise
|
|
12
|
+
*/
|
|
13
|
+
export function isValidUrl(url) {
|
|
14
|
+
if (!url || typeof url !== 'string') return false;
|
|
15
|
+
var trimmed = url.trim();
|
|
16
|
+
|
|
17
|
+
// Check if it matches URL pattern
|
|
18
|
+
if (URL_REGEX.test(trimmed)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check if it's a valid URL with protocol
|
|
23
|
+
try {
|
|
24
|
+
var urlObj = new URL(trimmed);
|
|
25
|
+
return !!urlObj.protocol && !!urlObj.hostname;
|
|
26
|
+
} catch (_unused) {
|
|
27
|
+
// Not a valid URL
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check if it looks like a domain (e.g., "google.com")
|
|
31
|
+
if (/^[\da-z][\w#%+./:=?@~-]*\.[\w#%+/:=?@~-]+$/i.test(trimmed)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Checks if text is a pure URL (single URL without other text)
|
|
39
|
+
* @param text - The text to check
|
|
40
|
+
* @returns true if text is a pure URL, false otherwise
|
|
41
|
+
*/
|
|
42
|
+
export function isPureUrl(text) {
|
|
43
|
+
if (!text || typeof text !== 'string') return false;
|
|
44
|
+
var trimmed = text.trim();
|
|
45
|
+
|
|
46
|
+
// Check if it's a single line
|
|
47
|
+
if (trimmed.includes('\n')) return false;
|
|
48
|
+
|
|
49
|
+
// Check if it's a valid URL
|
|
50
|
+
return isValidUrl(trimmed);
|
|
51
|
+
}
|
package/package.json
CHANGED