@inshapardaz/likhari-react 0.1.4-dev.35 → 0.1.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAS1D,OAAO,KAAK,EAAe,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,EAAwB,KAAK,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAOzH,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,cAAc,GAAG,YAAY,CAAC,CAAC;IACzD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,cAAc,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC;IACrD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IACrC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IAClD,iBAAiB,IAAI,OAAO,CAAC;IAC7B,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,KAAK,IAAI,IAAI,CAAC;CACf;AAQD,eAAO,MAAM,UAAU,uGAsHrB,CAAC"}
1
+ {"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAS1D,OAAO,KAAK,EAAe,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,EAAwB,KAAK,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAMzH,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,cAAc,GAAG,YAAY,CAAC,CAAC;IACzD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,cAAc,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC;IACrD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IACrC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IAClD,iBAAiB,IAAI,OAAO,CAAC;IAC7B,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,KAAK,IAAI,IAAI,CAAC;CACf;AAQD,eAAO,MAAM,UAAU,uGAqHrB,CAAC"}
@@ -14,7 +14,6 @@ import { EDITOR_NODES } from './nodes.js';
14
14
  import { editorTheme } from './theme/editorTheme.js';
15
15
  import { EditorThemeProvider } from './theme/EditorThemeProvider.js';
16
16
  import { Toolbar } from './components/Toolbar.js';
17
- import { LinkPastePlugin } from './plugins/LinkPastePlugin.js';
18
17
  function initialEditorStateJson(initialContent) {
19
18
  if (!initialContent)
20
19
  return undefined;
@@ -85,5 +84,5 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
85
84
  onSave?.(content, format);
86
85
  };
87
86
  const dir = locale === 'en' ? 'ltr' : 'rtl';
88
- return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, children: _jsx("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, onSave: handleSave, isDirty: isDirty, showSave: showSave }), _jsx("div", { className: "likhari-canvas", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "likhari-content-editable", dir: dir, "aria-label": "Editor content" }), placeholder: _jsx("div", { className: "likhari-placeholder", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }) }), config.history && _jsx(HistoryPlugin, {}), (config.lists.bullet || config.lists.numbered || config.lists.check) && _jsx(ListPlugin, {}), config.links && _jsx(LinkPlugin, {}), config.links && _jsx(LinkPastePlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange })] }) }) }));
87
+ return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, children: _jsx("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, onSave: handleSave, isDirty: isDirty, showSave: showSave }), _jsx("div", { className: "likhari-canvas", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "likhari-content-editable", dir: dir, "aria-label": "Editor content" }), placeholder: _jsx("div", { className: "likhari-placeholder", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }) }), config.history && _jsx(HistoryPlugin, {}), (config.lists.bullet || config.lists.numbered || config.lists.check) && _jsx(ListPlugin, {}), config.links && _jsx(LinkPlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange })] }) }) }));
89
88
  });
@@ -1 +1 @@
1
- {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAiO7E,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,YAAY,+BAqiB1E"}
1
+ {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AA4K7E,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,YAAY,+BA4W1E"}
@@ -1,24 +1,19 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { Menu, Select } from '@mantine/core';
4
4
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
5
- import { $getSelection, $isRangeSelection, $isElementNode, $setSelection, $getNearestNodeFromDOMNode, $createTextNode, KEY_MODIFIER_COMMAND, COMMAND_PRIORITY_NORMAL, FORMAT_TEXT_COMMAND, FORMAT_ELEMENT_COMMAND, INDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND, UNDO_COMMAND, REDO_COMMAND, SELECTION_CHANGE_COMMAND, CAN_UNDO_COMMAND, CAN_REDO_COMMAND, COMMAND_PRIORITY_LOW, } from 'lexical';
5
+ import { $getSelection, $isRangeSelection, $isElementNode, $setSelection, FORMAT_TEXT_COMMAND, FORMAT_ELEMENT_COMMAND, INDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND, UNDO_COMMAND, REDO_COMMAND, SELECTION_CHANGE_COMMAND, CAN_UNDO_COMMAND, CAN_REDO_COMMAND, COMMAND_PRIORITY_LOW, } from 'lexical';
6
6
  import { $setBlocksType } from '@lexical/selection';
7
7
  import { $createParagraphNode } from 'lexical';
8
8
  import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode } from '@lexical/rich-text';
9
9
  import { $isListNode, INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, REMOVE_LIST_COMMAND, } from '@lexical/list';
10
10
  import { $findMatchingParent } from '@lexical/utils';
11
- import { $createLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
12
- import { LinkDialog } from './LinkDialog.js';
13
- import { normalizeLinkUrl } from '../utils/linkUrl.js';
14
- import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconBold, IconClearFormatting, IconDeviceFloppy, IconDots, IconExternalLink, IconFeather, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconLinkOff, IconPencil, IconPhoto, IconPilcrow, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
11
+ import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconBold, IconClearFormatting, IconDeviceFloppy, IconDots, IconFeather, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconPhoto, IconPilcrow, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
15
12
  const INITIAL_STATE = {
16
13
  blockType: 'paragraph',
17
14
  activeFormats: new Set(),
18
15
  elementFormat: 'start',
19
16
  listType: null,
20
- isLink: false,
21
- linkUrl: '',
22
17
  canUndo: false,
23
18
  canRedo: false,
24
19
  };
@@ -54,15 +49,6 @@ function OverflowItem({ icon: Icon, label, active, onClick, }) {
54
49
  // Keep the editor's selection: a mousedown here would blur the canvas.
55
50
  onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: label }));
56
51
  }
57
- /** The link menu's contents — shared by the toolbar button and the right-click
58
- * menu: the URL (opens in a new tab), Edit link, Remove link. */
59
- function LinkMenuItems({ url, onEdit, onRemove }) {
60
- // Only offer the link as clickable if it's a URL the editor would have accepted.
61
- const safeUrl = normalizeLinkUrl(url);
62
- return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children: "Link" }), safeUrl ? (_jsx(Menu.Item, { component: "a", href: safeUrl, target: "_blank", rel: "noopener noreferrer", leftSection: _jsx(IconExternalLink, { size: ICON_SIZE, stroke: ICON_STROKE }), title: url, className: "likhari-link-menu-url", children: url })) : (
63
- // Not a URL the editor would accept (e.g. loaded from a document): show it, don't make it clickable.
64
- _jsx(Menu.Item, { disabled: true, title: url, className: "likhari-link-menu-url", children: url || '(no URL)' })), _jsx(Menu.Item, { leftSection: _jsx(IconPencil, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onEdit, children: "Edit link" }), _jsx(Menu.Item, { color: "red", leftSection: _jsx(IconLinkOff, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onRemove, children: "Remove link" })] }));
65
- }
66
52
  export function Toolbar({ config, onSave, isDirty, showSave }) {
67
53
  const [editor] = useLexicalComposerContext();
68
54
  const [state, setState] = useState(INITIAL_STATE);
@@ -82,9 +68,6 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
82
68
  else if ($isQuoteNode(element)) {
83
69
  blockType = 'quote';
84
70
  }
85
- const linkNode = $findMatchingParent(anchorNode, $isLinkNode);
86
- const isLink = Boolean(linkNode);
87
- const linkUrl = linkNode && $isLinkNode(linkNode) ? linkNode.getURL() : '';
88
71
  const activeFormats = new Set();
89
72
  ['bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript'].forEach((format) => {
90
73
  if (selection.hasFormat(format))
@@ -101,16 +84,9 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
101
84
  activeFormats,
102
85
  elementFormat,
103
86
  listType,
104
- isLink,
105
- linkUrl,
106
87
  }));
107
88
  });
108
89
  }, [editor]);
109
- // Selection changes alone miss edits that change the toolbar's state without
110
- // moving the caret (e.g. removing the link the caret is in).
111
- useEffect(() => {
112
- return editor.registerUpdateListener(() => updateToolbar());
113
- }, [editor, updateToolbar]);
114
90
  useEffect(() => {
115
91
  return editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
116
92
  updateToolbar();
@@ -174,8 +150,6 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
174
150
  menuSelectionRef.current = selection ? selection.clone() : null;
175
151
  });
176
152
  };
177
- const snapshotSelectionRef = useRef(snapshotSelection);
178
- snapshotSelectionRef.current = snapshotSelection;
179
153
  const runOverflowAction = (action) => () => {
180
154
  const saved = menuSelectionRef.current;
181
155
  if (saved)
@@ -183,93 +157,6 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
183
157
  action();
184
158
  editor.focus();
185
159
  };
186
- const [linkDialogOpen, setLinkDialogOpen] = useState(false);
187
- const [linkNeedsText, setLinkNeedsText] = useState(false);
188
- const openLinkDialog = useCallback(() => {
189
- // Snapshot first: the dialog's focus trap makes Lexical drop the selection.
190
- let collapsed = false;
191
- editor.getEditorState().read(() => {
192
- const selection = $getSelection();
193
- menuSelectionRef.current = selection ? selection.clone() : null;
194
- collapsed = $isRangeSelection(selection) && selection.isCollapsed();
195
- });
196
- setLinkNeedsText(collapsed && !state.isLink);
197
- setLinkDialogOpen(true);
198
- }, [editor, state.isLink]);
199
- const closeLinkDialog = () => {
200
- setLinkDialogOpen(false);
201
- editor.focus();
202
- };
203
- const restoreSelection = () => {
204
- const saved = menuSelectionRef.current;
205
- if (saved)
206
- editor.update(() => $setSelection(saved.clone()), { discrete: true });
207
- };
208
- const applyLink = ({ url, text }) => {
209
- restoreSelection();
210
- if (linkNeedsText) {
211
- // Nothing selected: insert a new link with its own text.
212
- editor.update(() => {
213
- const selection = $getSelection();
214
- if (!$isRangeSelection(selection))
215
- return;
216
- const link = $createLinkNode(url);
217
- link.append($createTextNode(text));
218
- selection.insertNodes([link]);
219
- });
220
- }
221
- else {
222
- editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
223
- }
224
- closeLinkDialog();
225
- };
226
- const removeLink = () => {
227
- restoreSelection();
228
- editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
229
- closeLinkDialog();
230
- };
231
- // Right-clicking a link in the text opens the same menu, at the pointer.
232
- const [linkContext, setLinkContext] = useState(null);
233
- useEffect(() => {
234
- if (!config.links)
235
- return;
236
- const onContextMenu = (event) => {
237
- const anchor = event.target?.closest?.('a');
238
- if (!anchor)
239
- return;
240
- let url = null;
241
- // Select the whole link first, so Edit / Remove act on it wherever the caret was.
242
- editor.update(() => {
243
- const node = $getNearestNodeFromDOMNode(anchor);
244
- const link = node ? $findMatchingParent(node, $isLinkNode) : null;
245
- if (!link || !$isLinkNode(link))
246
- return;
247
- url = link.getURL();
248
- link.select(0, link.getChildrenSize());
249
- }, { discrete: true });
250
- if (url === null)
251
- return;
252
- event.preventDefault();
253
- snapshotSelectionRef.current();
254
- setLinkContext({ x: event.clientX, y: event.clientY, url });
255
- };
256
- return editor.registerRootListener((root, previous) => {
257
- previous?.removeEventListener('contextmenu', onContextMenu);
258
- root?.addEventListener('contextmenu', onContextMenu);
259
- });
260
- }, [editor, config.links]);
261
- // Ctrl/Cmd+K opens the link dialog.
262
- useEffect(() => {
263
- if (!config.links)
264
- return;
265
- return editor.registerCommand(KEY_MODIFIER_COMMAND, (event) => {
266
- if (event.key.toLowerCase() !== 'k' || !(event.ctrlKey || event.metaKey))
267
- return false;
268
- event.preventDefault();
269
- openLinkDialog();
270
- return true;
271
- }, COMMAND_PRIORITY_NORMAL);
272
- }, [editor, config.links, openLinkDialog]);
273
160
  const withRefocus = (fn) => (value) => {
274
161
  fn(value);
275
162
  editor.focus();
@@ -318,7 +205,7 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
318
205
  const showFormattingGroup = headingLevels.length > 0 || config.lists.bullet || config.lists.numbered || config.lists.check || config.blocks.quote;
319
206
  const showInlineGroup = fmt.bold || fmt.italic || fmt.underline;
320
207
  const showAlignGroup = config.alignment.start || config.alignment.center || config.alignment.justify || config.alignment.left || config.alignment.right;
321
- const showStubInsertGroup = config.images.linked || config.images.embedded || config.poetry.enabled;
208
+ const showInsertPoetryGroup = config.links || config.images.linked || config.images.embedded || config.poetry.enabled;
322
209
  const showLanguageGroup = config.language.autocorrect || config.language.textCleanup || config.language.spellCheck;
323
210
  const showOverflowMenu = fmt.strikethrough || fmt.superscript || fmt.subscript || fmt.caseTransforms || fmt.clearFormatting || config.indent;
324
211
  const formattingOptions = [
@@ -339,16 +226,5 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
339
226
  ];
340
227
  const formattingValue = state.listType ?? state.blockType;
341
228
  const AlignIcon = ALIGN_ICONS[state.elementFormat] ?? IconAlignLeft;
342
- return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": "Formatting", children: [showSave && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarButton, { icon: IconDeviceFloppy, title: "Save", dirty: Boolean(isDirty), onClick: onSave }) })), config.history && (_jsxs("div", { className: "likhari-toolbar-group", children: [_jsx(ToolbarButton, { icon: IconArrowBackUp, title: "Undo", disabled: !state.canUndo, onClick: () => editor.dispatchCommand(UNDO_COMMAND, undefined) }), _jsx(ToolbarButton, { icon: IconArrowForwardUp, title: "Redo", disabled: !state.canRedo, onClick: () => editor.dispatchCommand(REDO_COMMAND, undefined) })] })), showFormattingGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: IconPilcrow, label: "Formatting", width: 148, value: formattingValue, data: formattingOptions, onChange: withRefocus((v) => applyFormatting(v)) }) })), showInlineGroup && (_jsxs("div", { className: "likhari-toolbar-group", children: [fmt.bold && (_jsx(ToolbarButton, { icon: IconBold, title: "Bold", active: state.activeFormats.has('bold'), onClick: () => formatText('bold') })), fmt.italic && (_jsx(ToolbarButton, { icon: IconItalic, title: "Italic", active: state.activeFormats.has('italic'), onClick: () => formatText('italic') })), fmt.underline && (_jsx(ToolbarButton, { icon: IconUnderline, title: "Underline", active: state.activeFormats.has('underline'), onClick: () => formatText('underline') }))] })), (config.font.family || config.font.size) && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.font.family && (_jsx(ToolbarSelect, { icon: IconTypography, label: "Font family", width: 110, value: null, placeholder: "Font", data: [], disabled: true })), config.font.size && (_jsx(ToolbarSelect, { icon: IconTextSize, label: "Font size", width: 90, value: null, placeholder: "Size", data: [], disabled: true }))] })), showAlignGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: AlignIcon, label: "Alignment", width: 138, value: state.elementFormat || 'start', data: alignOptions, onChange: withRefocus((v) => formatElement(v)) }) })), config.links && (_jsx("div", { className: "likhari-toolbar-group", children: state.isLink ? (
343
- // Caret is in a link: the button opens a menu to see, edit or remove it.
344
- _jsxs(Menu, { position: "bottom-start", withinPortal: true, shadow: "sm", width: 240, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": "true", "aria-pressed": "true", "aria-haspopup": "menu", "aria-label": "Link options", title: "Link options", onMouseDown: (e) => e.preventDefault(), children: _jsx(IconLink, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(LinkMenuItems, { url: state.linkUrl, onEdit: () => {
345
- restoreSelection();
346
- openLinkDialog();
347
- }, onRemove: removeLink }) })] })) : (_jsx(ToolbarButton, { icon: IconLink, title: "Insert link (Ctrl+K)", onClick: openLinkDialog })) })), config.links && (_jsxs(Menu, { opened: linkContext !== null, onChange: (opened) => {
348
- if (!opened)
349
- setLinkContext(null);
350
- }, position: "bottom-start", withinPortal: true, shadow: "sm", width: 240, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: linkContext?.x ?? -9999, top: linkContext?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsx(Menu.Dropdown, { children: _jsx(LinkMenuItems, { url: linkContext?.url ?? '', onEdit: () => {
351
- restoreSelection();
352
- openLinkDialog();
353
- }, onRemove: removeLink }) })] })), showStubInsertGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [(config.images.linked || config.images.embedded) && _jsx(StubButton, { icon: IconPhoto, title: "Insert image" }), config.poetry.enabled && _jsx(StubButton, { icon: IconFeather, title: "Poetry blocks" })] })), showLanguageGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.language.autocorrect && _jsx(StubButton, { icon: IconWand, title: "Auto-correct" }), config.language.textCleanup && _jsx(StubButton, { icon: IconSparkles, title: "Text cleanup" }), config.language.spellCheck && _jsx(StubButton, { icon: IconAbc, title: "Spell-checker" })] })), showOverflowMenu && (_jsxs(Menu, { position: "bottom-end", withinPortal: true, shadow: "sm", width: 210, closeOnItemClick: true, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "aria-label": "More formatting", title: "More formatting", onMouseDown: (e) => e.preventDefault(), children: _jsx(IconDots, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsxs(Menu.Dropdown, { children: [fmt.strikethrough && (_jsx(OverflowItem, { icon: IconStrikethrough, label: "Strikethrough", active: state.activeFormats.has('strikethrough'), onClick: runOverflowAction(() => formatText('strikethrough')) })), fmt.superscript && (_jsx(OverflowItem, { icon: IconSuperscript, label: "Superscript", active: state.activeFormats.has('superscript'), onClick: runOverflowAction(() => formatText('superscript')) })), fmt.subscript && (_jsx(OverflowItem, { icon: IconSubscript, label: "Subscript", active: state.activeFormats.has('subscript'), onClick: runOverflowAction(() => formatText('subscript')) })), fmt.caseTransforms && (_jsxs(_Fragment, { children: [_jsx(OverflowItem, { icon: IconLetterCaseUpper, label: "UPPERCASE", onClick: runOverflowAction(() => applyCaseTransform('upper')) }), _jsx(OverflowItem, { icon: IconLetterCaseLower, label: "lowercase", onClick: runOverflowAction(() => applyCaseTransform('lower')) }), _jsx(OverflowItem, { icon: IconLetterCase, label: "Capitalize", onClick: runOverflowAction(() => applyCaseTransform('capitalize')) })] })), fmt.clearFormatting && _jsx(OverflowItem, { icon: IconClearFormatting, label: "Clear formatting", onClick: runOverflowAction(clearFormatting) }), config.indent && (_jsxs(_Fragment, { children: [_jsx(OverflowItem, { icon: IconIndentDecrease, label: "Outdent", onClick: runOverflowAction(() => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined)) }), _jsx(OverflowItem, { icon: IconIndentIncrease, label: "Indent", onClick: runOverflowAction(() => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined)) })] }))] })] })), config.links && (_jsx(LinkDialog, { opened: linkDialogOpen, initialUrl: state.linkUrl, showTextField: linkNeedsText, onSubmit: applyLink, onRemove: state.isLink ? removeLink : undefined, onClose: closeLinkDialog }))] }));
229
+ return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": "Formatting", children: [showSave && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarButton, { icon: IconDeviceFloppy, title: "Save", dirty: Boolean(isDirty), onClick: onSave }) })), config.history && (_jsxs("div", { className: "likhari-toolbar-group", children: [_jsx(ToolbarButton, { icon: IconArrowBackUp, title: "Undo", disabled: !state.canUndo, onClick: () => editor.dispatchCommand(UNDO_COMMAND, undefined) }), _jsx(ToolbarButton, { icon: IconArrowForwardUp, title: "Redo", disabled: !state.canRedo, onClick: () => editor.dispatchCommand(REDO_COMMAND, undefined) })] })), showFormattingGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: IconPilcrow, label: "Formatting", width: 148, value: formattingValue, data: formattingOptions, onChange: withRefocus((v) => applyFormatting(v)) }) })), showInlineGroup && (_jsxs("div", { className: "likhari-toolbar-group", children: [fmt.bold && (_jsx(ToolbarButton, { icon: IconBold, title: "Bold", active: state.activeFormats.has('bold'), onClick: () => formatText('bold') })), fmt.italic && (_jsx(ToolbarButton, { icon: IconItalic, title: "Italic", active: state.activeFormats.has('italic'), onClick: () => formatText('italic') })), fmt.underline && (_jsx(ToolbarButton, { icon: IconUnderline, title: "Underline", active: state.activeFormats.has('underline'), onClick: () => formatText('underline') }))] })), (config.font.family || config.font.size) && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.font.family && (_jsx(ToolbarSelect, { icon: IconTypography, label: "Font family", width: 110, value: null, placeholder: "Font", data: [], disabled: true })), config.font.size && (_jsx(ToolbarSelect, { icon: IconTextSize, label: "Font size", width: 90, value: null, placeholder: "Size", data: [], disabled: true }))] })), showAlignGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: AlignIcon, label: "Alignment", width: 138, value: state.elementFormat || 'start', data: alignOptions, onChange: withRefocus((v) => formatElement(v)) }) })), showInsertPoetryGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.links && _jsx(StubButton, { icon: IconLink, title: "Insert link" }), (config.images.linked || config.images.embedded) && _jsx(StubButton, { icon: IconPhoto, title: "Insert image" }), config.poetry.enabled && _jsx(StubButton, { icon: IconFeather, title: "Poetry blocks" })] })), showLanguageGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.language.autocorrect && _jsx(StubButton, { icon: IconWand, title: "Auto-correct" }), config.language.textCleanup && _jsx(StubButton, { icon: IconSparkles, title: "Text cleanup" }), config.language.spellCheck && _jsx(StubButton, { icon: IconAbc, title: "Spell-checker" })] })), showOverflowMenu && (_jsxs(Menu, { position: "bottom-end", withinPortal: true, shadow: "sm", width: 210, closeOnItemClick: true, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "aria-label": "More formatting", title: "More formatting", onMouseDown: (e) => e.preventDefault(), children: _jsx(IconDots, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsxs(Menu.Dropdown, { children: [fmt.strikethrough && (_jsx(OverflowItem, { icon: IconStrikethrough, label: "Strikethrough", active: state.activeFormats.has('strikethrough'), onClick: runOverflowAction(() => formatText('strikethrough')) })), fmt.superscript && (_jsx(OverflowItem, { icon: IconSuperscript, label: "Superscript", active: state.activeFormats.has('superscript'), onClick: runOverflowAction(() => formatText('superscript')) })), fmt.subscript && (_jsx(OverflowItem, { icon: IconSubscript, label: "Subscript", active: state.activeFormats.has('subscript'), onClick: runOverflowAction(() => formatText('subscript')) })), fmt.caseTransforms && (_jsxs(_Fragment, { children: [_jsx(OverflowItem, { icon: IconLetterCaseUpper, label: "UPPERCASE", onClick: runOverflowAction(() => applyCaseTransform('upper')) }), _jsx(OverflowItem, { icon: IconLetterCaseLower, label: "lowercase", onClick: runOverflowAction(() => applyCaseTransform('lower')) }), _jsx(OverflowItem, { icon: IconLetterCase, label: "Capitalize", onClick: runOverflowAction(() => applyCaseTransform('capitalize')) })] })), fmt.clearFormatting && _jsx(OverflowItem, { icon: IconClearFormatting, label: "Clear formatting", onClick: runOverflowAction(clearFormatting) }), config.indent && (_jsxs(_Fragment, { children: [_jsx(OverflowItem, { icon: IconIndentDecrease, label: "Outdent", onClick: runOverflowAction(() => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined)) }), _jsx(OverflowItem, { icon: IconIndentIncrease, label: "Indent", onClick: runOverflowAction(() => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined)) })] }))] })] }))] }));
354
230
  }
@@ -234,14 +234,3 @@
234
234
  border-top: 1px solid var(--editor-line);
235
235
  margin: 1.5em 0;
236
236
  }
237
-
238
- /* Link menu: the URL item shows one truncated line. */
239
- .likhari-link-menu-url {
240
- max-width: 100%;
241
- }
242
-
243
- .likhari-link-menu-url .mantine-Menu-itemLabel {
244
- overflow: hidden;
245
- text-overflow: ellipsis;
246
- white-space: nowrap;
247
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-react",
3
- "version": "0.1.4-dev.35",
3
+ "version": "0.1.4",
4
4
  "description": "React implementation of the Likhari editor, built on @lexical/react.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,18 +0,0 @@
1
- export interface LinkDialogProps {
2
- opened: boolean;
3
- /** Existing link's URL when editing; empty when inserting. */
4
- initialUrl: string;
5
- /** Show a "Text" field — used when nothing is selected, so there is no text to link. */
6
- showTextField: boolean;
7
- onSubmit: (link: {
8
- url: string;
9
- text: string;
10
- }) => void;
11
- /** Present only when editing an existing link. */
12
- onRemove?: () => void;
13
- onClose: () => void;
14
- }
15
- /** Insert/edit-link dialog (UI spec §6). Mantine Modal supplies focus
16
- * trapping, Escape-to-close and the overlay. */
17
- export declare function LinkDialog({ opened, initialUrl, showTextField, onSubmit, onRemove, onClose }: LinkDialogProps): import("react").JSX.Element;
18
- //# sourceMappingURL=LinkDialog.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"LinkDialog.d.ts","sourceRoot":"","sources":["../../src/components/LinkDialog.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB,wFAAwF;IACxF,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,CAAC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxD,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;gDACgD;AAChD,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,eAAe,+BAgE7G"}
@@ -1,31 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
3
- import { Button, Group, Modal, TextInput } from '@mantine/core';
4
- import { normalizeLinkUrl } from '../utils/linkUrl.js';
5
- /** Insert/edit-link dialog (UI spec §6). Mantine Modal supplies focus
6
- * trapping, Escape-to-close and the overlay. */
7
- export function LinkDialog({ opened, initialUrl, showTextField, onSubmit, onRemove, onClose }) {
8
- const [url, setUrl] = useState(initialUrl);
9
- const [text, setText] = useState('');
10
- const [error, setError] = useState(null);
11
- useEffect(() => {
12
- if (opened) {
13
- setUrl(initialUrl);
14
- setText('');
15
- setError(null);
16
- }
17
- }, [opened, initialUrl]);
18
- const handleSubmit = (e) => {
19
- e.preventDefault();
20
- const normalized = normalizeLinkUrl(url);
21
- if (!normalized) {
22
- setError('Enter a valid http(s), mailto, tel or relative URL');
23
- return;
24
- }
25
- onSubmit({ url: normalized, text: text.trim() || normalized });
26
- };
27
- return (_jsx(Modal, { opened: opened, onClose: onClose, title: onRemove ? 'Edit link' : 'Insert link', centered: true, size: "sm", children: _jsxs("form", { onSubmit: handleSubmit, children: [_jsx(TextInput, { label: "URL", placeholder: "https://example.com", value: url, onChange: (e) => {
28
- setUrl(e.currentTarget.value);
29
- setError(null);
30
- }, error: error, "data-autofocus": true }), showTextField && (_jsx(TextInput, { mt: "sm", label: "Text", placeholder: "Link text (defaults to the URL)", value: text, onChange: (e) => setText(e.currentTarget.value) })), _jsxs(Group, { justify: "space-between", mt: "md", children: [onRemove ? (_jsx(Button, { variant: "subtle", color: "red", onClick: onRemove, children: "Remove link" })) : (_jsx("span", {})), _jsxs(Group, { gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", children: "Apply" })] })] })] }) }));
31
- }
@@ -1,12 +0,0 @@
1
- /**
2
- * Turns pasted URLs into links instead of inserting them as bare text:
3
- *
4
- * - a lone URL pasted over selected text links that text (the URL isn't inserted);
5
- * - a lone URL pasted at a caret becomes a link whose text is the URL;
6
- * - a single line of plain text containing URLs gets a link node per URL.
7
- *
8
- * Everything else — multi-line text, or anything copied with HTML (Lexical's
9
- * own paste already turns `<a>` into links) — falls through to the default paste.
10
- */
11
- export declare function LinkPastePlugin(): null;
12
- //# sourceMappingURL=LinkPastePlugin.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"LinkPastePlugin.d.ts","sourceRoot":"","sources":["../../src/plugins/LinkPastePlugin.tsx"],"names":[],"mappings":"AAaA;;;;;;;;;GASG;AACH,wBAAgB,eAAe,SAyC9B"}
@@ -1,50 +0,0 @@
1
- import { useEffect } from 'react';
2
- import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
- import { $createLinkNode, $toggleLink } from '@lexical/link';
4
- import { $createTextNode, $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, PASTE_COMMAND, } from 'lexical';
5
- import { splitPastedText } from '../utils/linkUrl.js';
6
- /**
7
- * Turns pasted URLs into links instead of inserting them as bare text:
8
- *
9
- * - a lone URL pasted over selected text links that text (the URL isn't inserted);
10
- * - a lone URL pasted at a caret becomes a link whose text is the URL;
11
- * - a single line of plain text containing URLs gets a link node per URL.
12
- *
13
- * Everything else — multi-line text, or anything copied with HTML (Lexical's
14
- * own paste already turns `<a>` into links) — falls through to the default paste.
15
- */
16
- export function LinkPastePlugin() {
17
- const [editor] = useLexicalComposerContext();
18
- useEffect(() => {
19
- return editor.registerCommand(PASTE_COMMAND, (event) => {
20
- const clipboard = 'clipboardData' in event ? event.clipboardData : null;
21
- const text = clipboard?.getData('text/plain');
22
- if (!clipboard || !text)
23
- return false;
24
- const segments = splitPastedText(text.trim());
25
- const hasLink = segments.some((s) => s.type === 'link');
26
- const isLoneUrl = segments.length === 1 && segments[0].type === 'link';
27
- const isPlainSingleLine = !clipboard.getData('text/html') && !/[\r\n]/.test(text.trim());
28
- if (!hasLink || !(isLoneUrl || isPlainSingleLine))
29
- return false;
30
- const selection = $getSelection();
31
- if (!$isRangeSelection(selection))
32
- return false;
33
- event.preventDefault();
34
- if (isLoneUrl && !selection.isCollapsed()) {
35
- $toggleLink(segments[0].url);
36
- return true;
37
- }
38
- const nodes = segments.map((segment) => {
39
- if (segment.type === 'text')
40
- return $createTextNode(segment.text);
41
- const link = $createLinkNode(segment.url);
42
- link.append($createTextNode(segment.text));
43
- return link;
44
- });
45
- selection.insertNodes(nodes);
46
- return true;
47
- }, COMMAND_PRIORITY_LOW);
48
- }, [editor]);
49
- return null;
50
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * Normalizes a URL typed into the link dialog, or returns null if it should
3
- * be rejected. Only http(s), mailto, tel, in-page (#), and relative URLs are
4
- * accepted — any other scheme (javascript:, data:, vbscript:, ...) is
5
- * refused so a pasted link can't carry script into exported HTML. A bare
6
- * domain like "example.com/page" gets https:// prefixed.
7
- */
8
- export declare function normalizeLinkUrl(input: string): string | null;
9
- export type PastedSegment = {
10
- type: 'text';
11
- text: string;
12
- } | {
13
- type: 'link';
14
- text: string;
15
- url: string;
16
- };
17
- /**
18
- * Splits pasted plain text into text and link segments. Only explicit URLs
19
- * count (http(s)://, mailto:, or a www. prefix) — a bare "file.txt" or
20
- * "example.com" is left as text, since pasted prose is full of those. Trailing
21
- * sentence punctuation stays outside the link, and anything normalizeLinkUrl
22
- * rejects stays text.
23
- */
24
- export declare function splitPastedText(text: string): PastedSegment[];
25
- //# sourceMappingURL=linkUrl.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"linkUrl.d.ts","sourceRoot":"","sources":["../../src/utils/linkUrl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc7D;AAED,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAKzG;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,EAAE,CA0B7D"}
@@ -1,62 +0,0 @@
1
- /**
2
- * Normalizes a URL typed into the link dialog, or returns null if it should
3
- * be rejected. Only http(s), mailto, tel, in-page (#), and relative URLs are
4
- * accepted — any other scheme (javascript:, data:, vbscript:, ...) is
5
- * refused so a pasted link can't carry script into exported HTML. A bare
6
- * domain like "example.com/page" gets https:// prefixed.
7
- */
8
- export function normalizeLinkUrl(input) {
9
- const url = input.trim();
10
- if (!url)
11
- return null;
12
- if (/^(https?:\/\/|mailto:|tel:)\S+$/i.test(url))
13
- return url;
14
- if (/^(#|\/|\.\.?\/)\S*$/.test(url))
15
- return url;
16
- // Any other explicit scheme is rejected.
17
- if (/^[a-z][a-z0-9+.-]*:/i.test(url))
18
- return null;
19
- // Looks like a bare domain (no spaces, has a dot in its host part).
20
- if (/^[^\s/?#]+\.[^\s/?#]+(?:[/?#]\S*)?$/.test(url))
21
- return `https://${url}`;
22
- return null;
23
- }
24
- const PASTED_URL_PATTERN = /(?:https?:\/\/|mailto:|www\.)[^\s<>"']+/gi;
25
- const TRAILING_PUNCTUATION = /[.,;:!?)\]}]+$/;
26
- /**
27
- * Splits pasted plain text into text and link segments. Only explicit URLs
28
- * count (http(s)://, mailto:, or a www. prefix) — a bare "file.txt" or
29
- * "example.com" is left as text, since pasted prose is full of those. Trailing
30
- * sentence punctuation stays outside the link, and anything normalizeLinkUrl
31
- * rejects stays text.
32
- */
33
- export function splitPastedText(text) {
34
- const segments = [];
35
- let last = 0;
36
- const pushText = (value) => {
37
- if (!value)
38
- return;
39
- const previous = segments[segments.length - 1];
40
- if (previous?.type === 'text')
41
- previous.text += value;
42
- else
43
- segments.push({ type: 'text', text: value });
44
- };
45
- for (const match of text.matchAll(PASTED_URL_PATTERN)) {
46
- const start = match.index ?? 0;
47
- const raw = match[0];
48
- const trimmed = raw.replace(TRAILING_PUNCTUATION, '');
49
- const url = normalizeLinkUrl(/^www\./i.test(trimmed) ? `https://${trimmed}` : trimmed);
50
- pushText(text.slice(last, start));
51
- if (url && trimmed.length > 'www.'.length) {
52
- segments.push({ type: 'link', text: trimmed, url });
53
- pushText(raw.slice(trimmed.length));
54
- }
55
- else {
56
- pushText(raw);
57
- }
58
- last = start + raw.length;
59
- }
60
- pushText(text.slice(last));
61
- return segments;
62
- }