fluxer/fluxer_app/src/utils/PasteSegmentUtils.ts
2026-01-01 21:05:54 +00:00

118 lines
3.1 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {MentionSegment} from './TextareaSegmentManager';
const MARKDOWN_MENTION_PATTERN = /<(@|#|@&)([a-zA-Z0-9]+)>/g;
const EMOJI_MARKDOWN_PATTERN = /<(a)?:([^:]+):([a-zA-Z0-9]+)>/g;
interface PastedSegmentInfo {
displayText: string;
actualText: string;
type: MentionSegment['type'];
id: string;
start: number;
end: number;
}
export interface LookupFunctions {
userById: (id: string) => {id: string; tag: string} | null;
channelById: (id: string) => {id: string; name: string} | null;
roleById: (id: string) => {id: string; name: string} | null;
emojiById: (id: string) => {id: string; name: string; uniqueName: string} | null;
}
export function detectPastedSegments(
pastedText: string,
pastePosition: number,
lookups: LookupFunctions,
): Array<PastedSegmentInfo> {
const segments: Array<PastedSegmentInfo> = [];
let match: RegExpExecArray | null;
MARKDOWN_MENTION_PATTERN.lastIndex = 0;
while ((match = MARKDOWN_MENTION_PATTERN.exec(pastedText)) !== null) {
const [fullMatch, prefix, id] = match;
const start = pastePosition + match.index;
const end = start + fullMatch.length;
let type: MentionSegment['type'];
let displayText: string | null = null;
if (prefix === '@') {
type = 'user';
const user = lookups.userById(id);
if (user) {
displayText = `@${user.tag}`;
}
} else if (prefix === '#') {
type = 'channel';
const channel = lookups.channelById(id);
if (channel) {
displayText = `#${channel.name}`;
}
} else if (prefix === '@&') {
type = 'role';
const role = lookups.roleById(id);
if (role) {
displayText = `@${role.name}`;
}
} else {
continue;
}
if (displayText) {
segments.push({
displayText,
actualText: fullMatch,
type,
id,
start,
end,
});
}
}
EMOJI_MARKDOWN_PATTERN.lastIndex = 0;
while ((match = EMOJI_MARKDOWN_PATTERN.exec(pastedText)) !== null) {
const [fullMatch, , , emojiId] = match;
const emoji = lookups.emojiById(emojiId);
if (emoji) {
const start = pastePosition + match.index;
const end = start + fullMatch.length;
const overlaps = segments.some((seg) => start < seg.end && end > seg.start);
if (!overlaps) {
segments.push({
displayText: `:${emoji.name}:`,
actualText: fullMatch,
type: 'emoji',
id: emoji.id,
start,
end,
});
}
}
}
return segments.sort((a, b) => a.start - b.start);
}