refactor(mention): extract shared escapeHtml helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,31 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
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';
|
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('<script>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('escapes double quote', () => {
|
||||||
|
expect(escapeHtml('say "hi"')).toBe('say "hi"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string unchanged', () => {
|
||||||
|
expect(escapeHtml('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('escapes ampersand before other entities to avoid double-encoding', () => {
|
||||||
|
expect(escapeHtml('a&<b')).toBe('a&<b');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ─── detectMention ────────────────────────────────────────────────────────────
|
// ─── detectMention ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('detectMention', () => {
|
describe('detectMention', () => {
|
||||||
|
|||||||
@@ -44,6 +44,18 @@ export function extractContent(
|
|||||||
return { content: text, mentionedUserIds: [...seen] };
|
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('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a comment body as safe HTML:
|
* Renders a comment body as safe HTML:
|
||||||
* 1. Escapes all HTML-special characters in the raw content
|
* 1. Escapes all HTML-special characters in the raw content
|
||||||
@@ -51,19 +63,11 @@ export function extractContent(
|
|||||||
* 3. Converts newlines to <br>
|
* 3. Converts newlines to <br>
|
||||||
*/
|
*/
|
||||||
export function renderBody(content: string, mentions: MentionDTO[]): string {
|
export function renderBody(content: string, mentions: MentionDTO[]): string {
|
||||||
let escaped = content
|
let escaped = escapeHtml(content);
|
||||||
.replaceAll('&', '&')
|
|
||||||
.replaceAll('<', '<')
|
|
||||||
.replaceAll('>', '>')
|
|
||||||
.replaceAll('"', '"');
|
|
||||||
|
|
||||||
for (const mention of mentions) {
|
for (const mention of mentions) {
|
||||||
const displayName = `${mention.firstName} ${mention.lastName}`.trim();
|
const displayName = `${mention.firstName} ${mention.lastName}`.trim();
|
||||||
const escapedDisplayName = displayName
|
const escapedDisplayName = escapeHtml(displayName);
|
||||||
.replaceAll('&', '&')
|
|
||||||
.replaceAll('<', '<')
|
|
||||||
.replaceAll('>', '>')
|
|
||||||
.replaceAll('"', '"');
|
|
||||||
const span = `<span class="mention" data-user-id="${mention.id}">@${escapedDisplayName}</span>`;
|
const span = `<span class="mention" data-user-id="${mention.id}">@${escapedDisplayName}</span>`;
|
||||||
escaped = escaped.replaceAll(`@${escapedDisplayName}`, span);
|
escaped = escaped.replaceAll(`@${escapedDisplayName}`, span);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user