Skip to content

Commit

Permalink
feat(RichTextEditor): enhance PastePlugin to support link segments an…
Browse files Browse the repository at this point in the history
…d improve text processing
  • Loading branch information
olafsulich committed Jan 14, 2025
1 parent a681764 commit 810d412
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ interface PastePluginProps {
}

interface Segment {
/** Type of the segment - either plain text or a mention */
type: 'text' | 'mention';
/** Content of the segment - for mentions this is the username without @ */
/** Type of the segment - either plain text, mention, or link */
type: 'text' | 'mention' | 'link';
/** Content of the segment */
content: string;
/** URL for link segments */
url?: string;
}

/**
Expand Down Expand Up @@ -79,19 +81,54 @@ export const PastePlugin = ({getMentionCandidates}: PastePluginProps) => {
* Creates a segment object with the specified type and content.
* Used to standardize segment creation throughout the plugin.
*/
const createSegment = (type: 'text' | 'mention', content: string): Segment => ({
const createSegment = (type: Segment['type'], content: string, url?: string): Segment => ({
type,
content,
...(url && {url}),
});

/**
* Processes plain text content and splits it into segments.
* Each segment is either a mention (if the user exists) or plain text.
* Each segment is either a mention, link, or plain text.
* Preserves the original text structure including spaces and formatting.
*/
const processPlainTextSegments = (text: string, availableUsers: User[]): Segment[] => {
const mentions = text.match(/@[\w]+/g) || [];
const segments: Segment[] = [];
let lastIndex = 0;

// First, find all URLs and create segments
const urlMatches = Array.from(text.matchAll(URL_REGEX));
urlMatches.forEach(match => {
const url = match[0];
const urlIndex = match.index!;

// Add text before URL if exists
if (urlIndex > lastIndex) {
const textBefore = text.slice(lastIndex, urlIndex);
const mentionSegments = processMentionSegments(textBefore, availableUsers);
segments.push(...mentionSegments);
}

// Add URL segment
segments.push(createSegment('link', url, url));
lastIndex = urlIndex + url.length;
});

// Process remaining text for mentions
if (lastIndex < text.length) {
const remainingText = text.slice(lastIndex);
const mentionSegments = processMentionSegments(remainingText, availableUsers);
segments.push(...mentionSegments);
}

return segments;
};

/**
* Processes text for mentions and returns segments
*/
const processMentionSegments = (text: string, availableUsers: User[]): Segment[] => {
const mentions = text.match(/@[\w]+/g) || [];
if (mentions.length === 0) {
return [createSegment('text', text)];
}
Expand Down Expand Up @@ -129,14 +166,47 @@ export const PastePlugin = ({getMentionCandidates}: PastePluginProps) => {
}

segments.forEach(segment => {
if (segment.type === 'text') {
selection.insertText(segment.content);
return;
switch (segment.type) {
case 'text':
selection.insertText(segment.content);
break;
case 'mention':
const mentionNode = $createMentionNode('@', segment.content);
selection.insertNodes([mentionNode, $createTextNode(' ')]);
break;
case 'link':
selection.insertText(createMarkdownLink(segment.url!, segment.content));
break;
}
});
};

const mentionNode = $createMentionNode('@', segment.content);
selection.insertNodes([mentionNode, $createTextNode(' ')]);
/**
* Processes HTML links from pasted content.
* Creates markdown links preserving both href and text content.
*/
const handleLinksFromHtml = (doc: Document): boolean => {
const links = doc.querySelectorAll('a');
if (links.length === 0) {
return false;
}

const selection = $getSelection();
if (!selection) {
return false;
}

links.forEach(link => {
const href = link.getAttribute('href');
const text = link.textContent?.trim();

if (href && text) {
selection.insertText(createMarkdownLink(href, text));
selection.insertText(' ');
}
});

return true;
};

/**
Expand All @@ -158,9 +228,13 @@ export const PastePlugin = ({getMentionCandidates}: PastePluginProps) => {
try {
if (htmlContent) {
const parser = new DOMParser();
const mentions = parser.parseFromString(htmlContent, 'text/html').querySelectorAll('[data-lexical-mention]');
const doc = parser.parseFromString(htmlContent, 'text/html');

if (mentions.length > 0) {
const mentions = doc.querySelectorAll('[data-lexical-mention]');
const handledMentions = mentions.length > 0;
const handledLinks = handleLinksFromHtml(doc);

if (handledMentions || handledLinks) {
mentions.forEach(mention => handleMentionFromHtml(mention, availableUsers));
return true;
}
Expand All @@ -184,3 +258,14 @@ export const PastePlugin = ({getMentionCandidates}: PastePluginProps) => {

return null;
};

// URL regex that matches most common URL formats
const URL_REGEX =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/gi;

/**
* Creates a markdown link from a URL and optional text
*/
const createMarkdownLink = (url: string, text?: string): string => {
return `[${text || url}](${url})`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
STRIKETHROUGH,
UNORDERED_LIST,
QUOTE,
LINK,
} from '@lexical/markdown';

export const markdownTransformers = [
Expand All @@ -42,5 +41,4 @@ export const markdownTransformers = [
ITALIC_STAR,
STRIKETHROUGH,
QUOTE,
LINK,
];

0 comments on commit 810d412

Please sign in to comment.