feat: person @mentions edit-mode infrastructure (PR-B1, #362) #369

Merged
marcel merged 26 commits from feat/person-mentions-issue-362-frontend-b1 into main 2026-04-29 08:05:49 +02:00
2 changed files with 39 additions and 11 deletions
Showing only changes of commit c7013f4902 - Show all commits

View File

@@ -1,7 +1,31 @@
import { describe, it, expect } from 'vitest';
import { detectMention, extractContent, renderBody } from './mention';
import { detectMention, escapeHtml, extractContent, renderBody } from './mention';
import type { MentionDTO } from '$lib/types';
// ─── escapeHtml ───────────────────────────────────────────────────────────────
describe('escapeHtml', () => {
it('escapes ampersand', () => {
expect(escapeHtml('AT&T')).toBe('AT&T');
});
it('escapes less-than and greater-than', () => {
expect(escapeHtml('<script>')).toBe('&lt;script&gt;');
});
it('escapes double quote', () => {
expect(escapeHtml('say "hi"')).toBe('say &quot;hi&quot;');
});
it('returns empty string unchanged', () => {
expect(escapeHtml('')).toBe('');
});
it('escapes ampersand before other entities to avoid double-encoding', () => {
expect(escapeHtml('a&<b')).toBe('a&amp;&lt;b');
});
});
// ─── detectMention ────────────────────────────────────────────────────────────
describe('detectMention', () => {

View File

@@ -44,6 +44,18 @@ export function extractContent(
return { content: text, mentionedUserIds: [...seen] };
}
/**
* Escapes the four HTML-special characters that can break out of text content
* or attribute values. & must be escaped first to avoid double-encoding.
*/
export function escapeHtml(str: string): string {
return str
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
}
/**
* Renders a comment body as safe HTML:
* 1. Escapes all HTML-special characters in the raw content
@@ -51,19 +63,11 @@ export function extractContent(
* 3. Converts newlines to <br>
*/
export function renderBody(content: string, mentions: MentionDTO[]): string {
let escaped = content
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
let escaped = escapeHtml(content);
for (const mention of mentions) {
const displayName = `${mention.firstName} ${mention.lastName}`.trim();
const escapedDisplayName = displayName
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
const escapedDisplayName = escapeHtml(displayName);
const span = `<span class="mention" data-user-id="${mention.id}">@${escapedDisplayName}</span>`;
escaped = escaped.replaceAll(`@${escapedDisplayName}`, span);
}