0c09c0cdd4
Build and deploy / deploy (push) Successful in 37s
Le "@Priape" écrit en composant une question n'apparaît plus dans la phrase affichée — stripMentions() le retire du texte (nettoyage des espaces qu'il laisse) et le montre à part sous forme de badges (MentionTags, avatar + pseudo), un peu comme une réponse, à la fois dans la liste des Archontes et sur la question projetée sur /char. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
// Mentions "@Pseudo" dans le texte d'une question du Char — repère qui est
|
|
// visé par une question sans ajouter de colonne : le pseudo mentionné est
|
|
// retrouvé par simple correspondance de texte contre la liste des membres
|
|
// (pas de state séparé à synchroniser avec une édition du texte).
|
|
|
|
export type MentionRange = { start: number; end: number; pseudo: string };
|
|
|
|
function isWordChar(char: string | undefined): boolean {
|
|
return !!char && /[\p{L}\p{N}_]/u.test(char);
|
|
}
|
|
|
|
// Les pseudos les plus longs sont recherchés en premier pour qu'un pseudo
|
|
// qui en préfixe un autre (ex. "Priape" / "Priapee") ne vole pas le match
|
|
// du plus long à l'endroit où il apparaît vraiment.
|
|
export function findMentionRanges(text: string, pseudos: string[]): MentionRange[] {
|
|
const candidates = [...new Set(pseudos)].filter(Boolean).sort((a, b) => b.length - a.length);
|
|
const lowerText = text.toLowerCase();
|
|
const claimed = new Array<boolean>(text.length).fill(false);
|
|
const ranges: MentionRange[] = [];
|
|
|
|
for (const pseudo of candidates) {
|
|
const needle = `@${pseudo.toLowerCase()}`;
|
|
let fromIndex = 0;
|
|
while (fromIndex <= lowerText.length) {
|
|
const idx = lowerText.indexOf(needle, fromIndex);
|
|
if (idx === -1) break;
|
|
fromIndex = idx + 1;
|
|
|
|
const end = idx + needle.length;
|
|
if (isWordChar(text[end])) continue;
|
|
if (claimed.slice(idx, end).some(Boolean)) continue;
|
|
|
|
for (let i = idx; i < end; i++) claimed[i] = true;
|
|
ranges.push({ start: idx, end, pseudo });
|
|
}
|
|
}
|
|
|
|
return ranges.sort((a, b) => a.start - b.start);
|
|
}
|
|
|
|
export function extractMentionedPseudos(text: string, pseudos: string[]): string[] {
|
|
return findMentionRanges(text, pseudos).map((range) => range.pseudo);
|
|
}
|
|
|
|
// Retire les "@Pseudo" du texte affiché (ils vivent à part, en métadonnée à
|
|
// côté de la question — un peu comme une réponse — plutôt qu'au milieu de
|
|
// la phrase) et renvoie les pseudos visés, dans leur ordre d'apparition et
|
|
// sans doublon si une même personne est taguée plusieurs fois.
|
|
export function stripMentions(text: string, pseudos: string[]): { cleanText: string; mentioned: string[] } {
|
|
const ranges = findMentionRanges(text, pseudos);
|
|
if (ranges.length === 0) return { cleanText: text, mentioned: [] };
|
|
|
|
let cleanText = "";
|
|
let cursor = 0;
|
|
const seen = new Set<string>();
|
|
const mentioned: string[] = [];
|
|
for (const range of ranges) {
|
|
cleanText += text.slice(cursor, range.start);
|
|
if (!seen.has(range.pseudo)) {
|
|
seen.add(range.pseudo);
|
|
mentioned.push(range.pseudo);
|
|
}
|
|
cursor = range.end;
|
|
}
|
|
cleanText += text.slice(cursor);
|
|
|
|
// Nettoie les espaces laissés par les mentions retirées (double espace
|
|
// au milieu de la phrase, espace parasite avant une ponctuation, bords).
|
|
cleanText = cleanText
|
|
.replace(/[ \t]{2,}/g, " ")
|
|
.replace(/\s+([,.!?;:])/g, "$1")
|
|
.trim();
|
|
|
|
return { cleanText, mentioned };
|
|
}
|